Computação Gráfica I

Trabalho Prático

Exercícios



Parte II - Sombreamento/Shading

Como já sabemos como visualizar malhas, nesta segunda parte, ao invés de usar um objeto primitivo da GLUT como o teapot, aplicaremos sombreamento sobre uma malha modelada com nossa própria classe Mesh. À primeira vista, vocês podem pensar que fazendo o display com as configurações de shading usadas para o teapot bastaria. Tentem!

1. O que acontece quando se aplica sombreamento a uma malha ordinária.

O rendering exibe uma cor homogênea em toda a superfície da malha, parecendo um desenho em 2D. Isso acontece porque os vetores normais da superfície da malha (triangulos ou vértices) não são conhecidos. É exatamente isso que acontecia também com as primeiras malhas que criamos nas aulas anteriores (tetraedro, cubo, perfis de revolução, etc).
Lembrem-se da parte teórica da aula de sombreamento. O professor falou várias vezes que o vetor normal era usado para calcular a intensidade de reflexão da luz.

Os exercícios restantes deste tutorial visam criar uma estrutura de dados para armazenar a informação de vetores normais de faces e vértices. Esta estrutura de dados é alimentada com valores e então passada para a OpenGL que a utilizará para o cálculo de sombreamento.

2. Atualizem suas classes Mesh adicionando o seguinte:

Em mesh.h antes da declaração da classe:

struct Normal // define o vetor normal
{
float x,y,z;
};

struct Neighbors // define uma lista de triangulos vizinhos de um triangulo
{
Neighbors(){tri=0;nbr=0;}
~Neighbors(){delete [] tri;}

void addNeighbor(int triangle) // adiciona um vizinho na lista
{
for (int i=0;i<nbr;i++) if (tri[i]==triangle) return;
int *temp=new int[nbr+1];
for (int i=0;i<nbr;i++) temp[i]=tri[i];
temp[nbr]=triangle;
delete [] tri;
tri=temp;
nbr++;
}

int *tri; // lista de vizinhos
int nbr; // tamanho da lista
};

A lista de vizinhos serve para acelerar o cálculo das normais dos vértices, serve para evitar que a lista de triangulos seja percorrida n*n vezes (sendo n o número de triângulos) a cada vez que se deseja recalcular as normais da malha.

Em mesh.h dentro do corpo da classe Mesh:

// declaracao das novas funcoes e variaveis necessarias ao calculo das normais
void initNeighbors();
void calcNormals();
float invSqrt (float x);
void calcNormal(Vertex& p1,Vertex& p2,Vertex& p3,Normal& n);
void normalize(Normal& n);
Neighbors *vertNeigh; // triangles that are adjacent to a vertex
Normal *vertNorm;
Normal *triNorm;

Em mesh.h depois do corpo da classe:

// definicao das novas funcoes necessarias ao calculo das normais 

inline float Mesh::invSqrt (float x) // raiz inversa para normalizacao
{
float xhalf = 0.5f*x;
int i = *(int*)&x;
i = 0x5f3759df - (i >> 1); // This line hides a LOT of math!
x = *(float*)&i;
x = x*(1.5f - xhalf*x*x); // repeat this statement for a better approximation
return x;
}

inline void Mesh::calcNormal(Vertex& p1,Vertex& p2,Vertex& p3,Normal& n) // dados 3 vertices, calcula a normal do triangulo formado por eles
{
Vertex v1;
Vertex v2;
v1.x = p2.x-p1.x;
v1.y = p2.y-p1.y;
v1.z = p2.z-p1.z;

v2.x = p3.x-p2.x;
v2.y = p3.y-p2.y;
v2.z = p3.z-p2.z;

n.x = v1.y*v2.z - v1.z*v2.y;
n.y = v1.z*v2.x - v1.x*v2.z;
n.z = v1.x*v2.y - v1.y*v2.x;

// normalization step
float l=invSqrt(n.x*n.x+n.y*n.y+n.z*n.z);

// Possibly unnecessary
if (l==0.0) l=1.0;

n.x*=l;
n.y*=l;
n.z*=l;
}

inline
void Mesh::normalize(Normal& n) // normaliza qualquer vetor n dado
{
float length=invSqrt(n.x*n.x+n.y*n.y+n.z*n.z);

// Possibly unnecessary
if(length == 0.0) length = 1.0;

n.x *= length;
n.y *= length;
n.z *= length;
}


Em mesh.cpp modifiquem estas duas funções adicionando as linhas abaixo (especificadas entre "ADD THIS" e "END ADD THIS"):

void Mesh::initVerts(int p_nbrVerts)
{
  nbrVerts=p_nbrVerts;
  verts =new Vertex[nbrVerts];
  // ADD THIS
  vertNorm=new Normal[nbrVerts];
  vertNeigh=new Neighbors[nbrVerts];
  // END ADD THIS
}

void Mesh::initTris(int p_nbrTris)
{
  nbrTris=p_nbrTris;
  tris=new Triangle[nbrTris];
  // ADD THIS
  triNorm=new Normal[nbrTris];
  // END ADD THIS
}

Também em mesh.cpp adicione estas funções:

void Mesh::initNeighbors() //carrega a estrutura de dados de vizinhanca da malha
{
  for (int t=0;t<nbrTris;t++)
  {
    for (int v=0;v<3;v++)
    {
      vertNeigh[tris[t].vert[v]].addNeighbor(t);
    }
  }
}

void Mesh::calcNormals() // calcula todas as normais da malha, carregando a estrutura de dados
{
  // First calculate the triangle normals
  for (int t=0;t<nbrTris;t++)
  {
    calcNormal(verts[tris[t].vert[0]],
               verts[tris[t].vert[1]],
               verts[tris[t].vert[2]],
               triNorm[t]);
  }
  // Now calculate the vertex normals
  for (int v=0;v<nbrVerts;v++)
  {
    vertNorm[v].x=vertNorm[v].y=vertNorm[v].z=0;
    for (int t=0;t<vertNeigh[v].nbr;t++)
    {
      vertNorm[v].x+=triNorm[vertNeigh[v].tri[t]].x;
      vertNorm[v].y+=triNorm[vertNeigh[v].tri[t]].y;
      vertNorm[v].z+=triNorm[vertNeigh[v].tri[t]].z;
    }
    normalize(vertNorm[v]);
  }
}

Em main.cpp adicionem isto logo após seus outros includes:

#include "dolphin.h"
// Lighting flag
bool smooth;

Mesh dolphin;

Substituam a função drawMesh atual por esta (agora estamos enviando também as normais para a OpenGL):

void drawMesh(Mesh& m)
{
if (smooth)
{
glVertexPointer(3,GL_FLOAT,0,m.verts);
glNormalPointer(GL_FLOAT,0,m.vertNorm);
glDrawElements (GL_TRIANGLES,m.nbrTris*3,GL_UNSIGNED_INT,m.tris);
}
else
{
glBegin(GL_TRIANGLES);
for (int i=0;i<m.nbrTris;i++)
{
glNormal3f(m.triNorm[i].x,m.triNorm[i].y,m.triNorm[i].z);
for (int v=0;v<3;v++)
{
glVertex3f(m.verts[m.tris[i].vert[v]].x,
m.verts[m.tris[i].vert[v]].y,
m.verts[m.tris[i].vert[v]].z);
}
}
glEnd();
}
}
Em main.cpp adicionem isto no final das suas funções init(). Esta parte serve a criar efetivamente a malha do golfinho a partir dos dados em dolphin.h:
// habilita o vetor de normais para o rendering
glEnableClientState(GL_NORMAL_ARRAY);

smooth=false;

if (smooth)
{
glShadeModel(GL_SMOOTH);
}
else
{
glShadeModel(GL_FLAT); // Enables Smooth Color Shading (default)
}

// cria as estruturas com o tamanho certo
dolphin.initVerts(NUM_VERTICES); //NUM_VERTICES é uma constante definida em dolphin.h
dolphin.initTris(NUM_POLYGONS); //NUM_POLYGONS é uma constante definida em dolphin.h

// Inicializa os vertices
for (int v=0;v<dolphin.nbrVerts;v++)
{
dolphin.verts[v].x=mesh_vertices[0+v*3]; //mesh_vertices é um array definido em dolphin.h
dolphin.verts[v].y=mesh_vertices[1+v*3];
dolphin.verts[v].z=mesh_vertices[2+v*3];
}
// Inicializa os triangulos
for (int t=0;t<dolphin.nbrTris;t++)
{
for (int v=0;v<3;v++)
{
dolphin.tris[t].vert[v]=dolph_tri[v+t*3]; //mesh_polygons é um array definido em dolphin.h
 }
}
// Agora podemos calcular as normais
dolphin.initNeighbors();
dolphin.calcNormals();

Finalmente, não esqueçam de chamar drawMesh(dolphin) na sua função display().

Pra testar, mudem o valor da variável bool smooth para trocar entre sombreamento flat e smooth (Gouraud). Podem usar o teclado ou mouse para fazer isso enquanto o programa está executando. O resultado deve ser algo parecido com isso (Esquerda: flat; Direita: smooth).