Projeto Virtual Worlds

Computação Gráfica 3D em C

Arquivos da Categoria: Texturas

Mudança de base e “bump mapping”

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

Dê uma olhada nas 3 texturas abaixo:

Textura original, mapa de normais e textura modificada.

Textura original, mapa de normais e textura modificada.

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

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

Visualmente, esses vetores correspondem ao diagrama abaixo:

Visualizando os vetores tangente e binormal.

Visualizando os vetores tangente e binormal.

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

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

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

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

Calculando a iluminação:

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

Antes e depois do "solavanco".

Antes e depois do “solavanco”.

O problema do bump mapping:

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

Bump Mapping e Parallax Mapping, lado a lado.

Bump Mapping e Parallax Mapping, lado a lado.

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

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

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

Anúncios

PNG Loader, de novo…

No agora extinto Lost in the e-Jungle eu havia postado a rotina para carga de arquivos PNG via libpng. Eu tinha um problema que não citei por lá: A função glTexImage2D precisa receber o formato específico da imagem carregada na memória. No meu caso, para facilitar a rotina, assumi que o formato seria GL_RGBA e fui em frente. Acontece que algumas imagens PNG têm formato diferente… Às vezes elas têm formato RGB, descartando o canal alpha. Noutras, são formatadas em grey scale com menos de 8 bits e, ainda, em outras vezes elas contém uma tabela de cores (palette), igualzinho ao que acontece quando vocẽ lida com arquivos GIF. Como lidar com todos esses formatos?

Felizmente a libpng nos ajuda. Abaixo, temos a rotina que carrega um arquivo PNG, mas que fornece um pixelmap sempre no formato RGBA:

#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>
#include <malloc.h>
#include <png.h>
#include <setjmp.h>

#define PNG_SIG_BYTES 8

/* A rotina devolve um ponteiro para o pixelmap
   e também os tamanhos (width e height) da imagem.
   Retornará NULL se houve algum problema. */
uint8_t *load_png(char *name, int *width, int *height)
{
  FILE *png_file;
  uint8_t header[PNG_SIG_BYTES];
  png_structp png_ptr;
  png_infop info_ptr, end_info;
  uint32_t bit_depth, color_type;
  uint32_t rowbytes;
  uint32_t numbytes;
  uint8_t *pixels = NULL;
  uint8_t **row_ptrs = NULL;
  int i;

  if ((png_file = fopen(name, "rb")) == NULL)
    return NULL;

  if (fread(header, PNG_SIG_BYTES, 1, png_file) != 1)
  {
    fclose(png_file);
    return NULL;
  }

  png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING,
                                   NULL, NULL, NULL);
  if (png_ptr == NULL)
  {
    fclose(png_file);
    return NULL;
  }

  info_ptr = png_create_info_struct(png_ptr);
  end_info = png_create_info_struct(png_ptr);

  if (setjmp(png_jmpbuf(png_ptr)))
  {
    if (pixels != NULL) free(pixels);
    if (row_ptrs != NULL) free(row_ptrs);

    png_destroy_read_struct(&png_ptr, &info_ptr, &end_info);
    fclose(png_file);

    return NULL;
  }

  png_init_io(png_ptr, png_file);
  png_set_sig_bytes(png_ptr, PNG_SIG_BYTES);
  png_read_info(png_ptr, info_ptr);

  *width = png_get_image_width(png_ptr, info_ptr);
  *height = png_get_image_height(png_ptr, info_ptr);

  /* Pega info sobre o pixelmap. */
  bit_depth = png_get_bit_depth(png_ptr, info_ptr);
  color_type = png_get_color_type(png_ptr, info_ptr);

  /* Se for grayscale e bits por componente < 8,
     converte para 8 bits por componente. */
  if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
    png_set_gray_1_2_4_to_8(png_ptr);

  /* Se for 16 bits por componente, converte para 8. */
  if (bit_depth == 16)
      png_set_strip_16(png_ptr);

  /* Se tiver paleta, converte para RGB */
  if (color_type == PNG_COLOR_TYPE_PALETTE)
    png_set_palette_to_rgb(png_ptr);
  else
  {
    /* Senão, se for grayscale, coloca ALPHA */
    if (color_type == PNG_COLOR_TYPE_GRAY ||
        color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
      png_set_gray_to_rgb(png_ptr);
  }

  /* Tem transparência? */
  if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS))
    /* Converte tRNS para ALPHA */
    png_set_tRNS_to_alpha(png_ptr);
  else
    /* Senão, coloca ALPHA = 1.0 */
    png_set_filler(png_ptr, 0xff, PNG_FILLER_AFTER);

  /* Atualiza a imagem lida. */
  png_read_update_info(png_ptr, info_ptr);

  /* Aloca espaço para a imagem e
     os ponteiros para as linhas. */
  rowbytes = png_get_rowbytes(png_ptr, info_ptr);
  numbytes = rowbytes*(*height);
  pixels   = malloc(numbytes);

  /* Esse longjmp() é um HACK, usando o jmpbuf
     da estrutura png_struct. */
  if (pixels == NULL)
    longjmp(png_ptr->jmpbuf, 1);

  row_ptrs = malloc((*height) * sizeof(uint8_t *));

  if (row_ptrs == NULL)
    longjmp(png_ptr->jmpbuf, 1);

  /* Prepara os ponteiros para as linhas
     de forma inversa em relação ao eixo y.
     OpenGL precisa disso! */
  for (i=0; i < *height; i++)
    row_ptrs[i] = pixels + ((*height) - 1 - i)*rowbytes;

  /* Lê as linhas */
  png_read_image(png_ptr, row_ptrs);

  /* Livra-se do vetor de ponteiros para as linhas. */
  free(row_ptrs);

  /* Destrói as estruturas da libpng e
     fecha o arquivo. */
  png_destroy_read_struct(&png_ptr, &info_ptr, &end_info);
  fclose(png_file);

  /* Retorna o pixelmap */
  return pixels;
}

Dessa forma vocẽ sempre terá um pixelmap RGBA, no final das contas.

É bom lembrar também que a função glTexImage2D espera pixelmaps com tamanho horizontal e vertical proporcionais a 2n nas versões do OpenGL inferiores a 2.0, incluindo a especificação OpenGL ES. Versões mais recentes suportam quaisquer tamanhos (até um certo limite, é claro), mas usar tamanhos diferentes de 2n tende a ser menos performático.