quinta-feira, 11 de julho de 2013

Mudança do Espaço Canônico para o de Tela – Computação Gráfica

O segundo trabalho da disciplina de Introdução a Computação Gráfica da UFPB lecionada pelo Prof. Christian Pagot tem como objetivo a implementação de transformações entre espaços de coordenadas. A atividade consiste em implementar o penúltimo estágio do Pipeline Gráfico, a mudança do espaço canônico (espaço delimitado pelas coordenadas de [-1,-1,-1] até [1,1,1] ) para o espaço de tela (espaço delimitado pelas coordenadas inteiras [0,0,0] até as dimensões da tela) para que possa ser feita a Rasterização (último estágio do pipeline), assunto do meu primeiro post.

Para maior facilidade de implementação do segundo e do possível terceiro trabalho, fiz algumas melhoras. São elas:

- Implementei uma biblioteca que gerenciasse toda interface com o usuário e implementação do pipeline gráfico ( Até agora apenas só os dois últimos passos do pipeline);
- Orientação a Objeto em C++;
- Criação dos módulos de Matrizes e Vetores para facilitar e abstrair algumas operações.

Segue os cabeçalhos de todos os módulos utilizados.

1:  #include "definitions.h"  
2:  #include "lib/Color.h"  
3:  #include "lib/Vertex.h"  
4:  #include "lib/Matrix.h"  
5:  #include "lib/Vector.h"  


Implementação

A implementação foi feita toda na função private void myglCanonicalToScreen(void) encontrada na classe MyGL. Seguindo a sequencia de passos:
  1. Inverter a coordenada Y ( Transformação de Escala );
  2. Escalar as coordenadas X e Y pela metade do tamanho da tela;
  3. Transladar pela metade do tamanho da tela nas coordenadas X e Y;
  4. Enfim, arrendondar e truncar os valores para que possa ser feita a rasterização corretamente.

Obs.: O desenhos nesse momento estarão invertidos, porem a tela tem o eixo Y direcionado para baixo fazendo assim com que o desenho seja desenhado na sua forma original.

Podemos perceber que toda sequencia consiste em duas matrizes a de translação e a de escala, são elas:
Translação

  
   Escala
Essas são as transpostas das matrizes de translação e de escala, pois eu estou implementando usando vetor linha. Implementei duas funções myglTranslate() e myglScale() que retorna as matrizes correspondentes.

1:  /********************************************************  
2:   * Transformações  
3:   ********************************************************/  
4:  void MyGL::myglTranslate(MYGLmatrix &matrix, float dx = 0, float dy = 0, float dz = 0, float dw = 1 )  
5:  {  
6:     MYGLmatrix temp{{1,0,0,0},  
7:                {0,1,0,0},  
8:                {0,0,1,0},  
9:                {dx,dy,dz,dw}};  
10:     matrix = temp;   
11:  }  
12:  void MyGL::myglScale(MYGLmatrix &matrix, float sx = 1, float sy = 1, float sz = 1, float sw = 1 )  
13:  {  
14:     MYGLmatrix temp{{sx,0,0,0},  
15:                {0,sy,0,0},  
16:                {0,0,sz,0},  
17:                {0,0,0,sw}};  
18:     matrix = temp;   
19:  }  

Segue abaixo o exemplo que eu utilizarei ainda no espaço canônico:


1. Inverter a coordenada Y

Invertemos as coordenadas Y de todos os vertices usando a matriz de escala seguinte:



myglScale(invertY, 1,-1);




Ilustração depois de invertemos as coordenadas Y dos vertices.



2. Escalamos as coordenadas X e Y com o valor da metade do tamanho da largura e altura da janela. Sendo IMAGE_WIDTH e IMAGE_HEIGHT constantes de largura e altura. Usando a seguinte matriz:


myglScale(scale,(IMAGE_WIDTH – 1) / 2,(IMAGE_HEIGHT – 1) / 2);






Na imagem estou supondo que o tamanho da tela é 511, ou seja, de 0 até 510.


3. Agora, transladamos as coordenadas X e Y com o mesmo valor usado no passo anterior. Segue a matriz utilizada:


myglTranslate(trans,(IMAGE_WIDTH – 1) / 2,(IMAGE_HEIGHT – 1) / 2, 0);





Exemplo após a translação:


4. Arrendondamos e truncamos usando a seguinte função myglRound(), logo abaixo sua implementação:

1:  // **************************************************************************  
2:  MYGLvector &MyGL::myglRound(MYGLvector &res)  
3:  {  
4:     for(int j = 0; j < res.getSize(); j++)  
5:        res[j] = (int)(res[j] + 0.5);  
6:     return res;  
7:  }  

Podemos também executar tudo de uma vez usando apenas uma matriz então basta multiplicarmos na ordem que o mais a direita seja a primeira transformação e o mais a esquerda a ultima transformação, porem estou implementando o vetor linha então temos que inverter essa ordem, ficando assim a primeira transformação mais a esquerda. Para isso sobrecarreguei o operador de multiplicação para tipo Matrix<float> que foi definido como MYGLmatrix. Com isso temos o exemplo após todas as transformações e o eixo de coordenada Y virado para baixo como é utilizado pela tela.

Matriz após as multiplicações:




Segue abaixo a implementação de toda a função myglCanonicalToScreen(void):

1:  // ********************************************************************  
2:  void MyGL::myglCanonicalToScreen(void)  
3:  {  
4:     MYGLmatrix trans(4,4); // translação  
5:     MYGLmatrix scale(4,4); // escala  
6:     MYGLmatrix invertY(4,4); // escala invertida  
7:    
8:     MYGLmatrix MatrixScreen(4,4); // Matriz resultado  
9:    
10:     // Aplica as transformações  
11:     myglTranslate(trans,(IMAGE_WIDTH - 1)/2.0f,(IMAGE_HEIGHT - 1)/2.0f);  
12:     myglScale(invertY,1.0f,-1.0f);  
13:     myglScale(scale,(IMAGE_WIDTH - 1)/2.0f,(IMAGE_HEIGHT - 1)/2.0f);  
14:    
15:     /* Estou usando a respresentação vetor coluna então usamos as   
16:       transpostas das matrizes e invertemos a ordem de multiplicação*/  
17:     MatrixScreen = invertY * scale * trans;  
18:    
19:     // Pegamos toda malha de vertices e multiplicamos pela matriz retornando  
20:     // os vertices já transformados e armazena atualiza os valores de todos eles  
21:     for(auto p = vectorVertex.begin(); p != vectorVertex.end(); p++ )     
22:     {     
23:        MYGLvector result(4);  
24:        result = p->getVector() * MatrixScreen;  
25:        p->setVector( myglRound(result) );  
26:     }  
27:  }  

Observação: O processo de Rasterização se encontra na função myglRasterization(void) as duas funções são executadas na função Flush(void). E a classe MyGL possui um vector de Vertex que é usada tanto na hora da mudança de espaço e na de desenhar.

Agora alguns printscreens para comparação dos resultados com o do OpenGL, vale lembrar que estou usando uma interface parecida com a do OpenGL:





















1:  //-----------------------------------------------------------------------------  
2:  void MyGlDraw(void)  
3:  {  
4:     //*************************************************************************  
5:     // Chame aqui as funções do mygl.h  
6:     //*************************************************************************  
7:     MyGL::ClearColor(0,0,0);  
8:     MyGL::Clear();  
9:    
10:     MyGL::setColor(255,255,255);  
11:    
12:     MyGL::Begin(MYGL_LINES);  
13:        MyGL::setVertex(-0.5f,-0.5f);  
14:        MyGL::setVertex( 0.5f,-0.5f);  
15:        MyGL::setVertex(0.5f,-0.5f);  
16:        MyGL::setVertex(0.0f,0.5f);  
17:        MyGL::setVertex(0.0f,0.5f);  
18:        MyGL::setVertex(-0.5f,-0.5f);  
19:     MyGL::End();  
20:       
21:     MyGL::Flush();  
22:  }  


1:  void display(void)  
2:  {  
3:     glClearColor(0.0f, 0.0f, 0.0f, 1.0f);  
4:     glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);  
5:     glViewport(0, 0, 512, 512);  
6:    
7:     glOrtho(-1.0f,1.0f,-1.0f,1.0f,-1.0f,1.0f);  
8:     glColor3f(1.0f,1.0f,1.0f);  
9:     // Alterar o trecho abaixo para desenhar os pontos, linhas e triãngulos   
10:     // a serem utilizados nas comparações.  
11:     glBegin(GL_LINES);  
12:        glVertex3f(-0.5f,-0.5f,0.0f);  
13:        glVertex3f( 0.5f,-0.5f,0.0f);  
14:        glVertex3f(0.5f,-0.5f,0.0f);  
15:        glVertex3f(0.0f,0.5f,0.0f);  
16:        glVertex3f(0.0f,0.5f,0.0f);  
17:        glVertex3f(-0.5f,-0.5f,0.0f);  
18:     glEnd();  
19:     glFlush();  
20:     glutSwapBuffers();  
21:     glutPostRedisplay();  
22:  }  






1:  //-----------------------------------------------------------------------------  
2:  void MyGlDraw(void)  
3:  {  
4:     //*************************************************************************  
5:     // Chame aqui as funções do mygl.h  
6:     //*************************************************************************  
7:     MyGL::ClearColor(0,0,0);  
8:     MyGL::Clear();  
9:    
10:     MyGL::Begin(MYGL_TRIANGLES);  
11:        MyGL::setColor(255,0,0);  
12:        MyGL::setVertex(-0.5f,-0.5f);  
13:        MyGL::setColor(0,255,0);  
14:        MyGL::setVertex(0.5f,-0.5f);  
15:        MyGL::setColor(0,0,255);  
16:        MyGL::setVertex(0.0f,0.5f);  
17:     MyGL::End();  
18:       
19:     MyGL::Flush();  
20:  }  



1:  //­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­  
2:  void display(void)  
3:  {  
4:     glClearColor(0.0f, 0.0f, 0.0f, 1.0f);  
5:     glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);  
6:     glViewport(0, 0, 512, 512);  
7:    
8:     glOrtho(-1.0f,1.0f,-1.0f,1.0f,-1.0f,1.0f);  
9:     // Alterar o trecho abaixo para desenhar os pontos, linhas e triãngulos   
10:     // a serem utilizados nas comparações.  
11:     glBegin(GL_TRIANGLES);  
12:        glColor3f(1.0f,0.0f,0.0f);  
13:        glVertex3f(-0.5f,-0.5f,0.0f);  
14:        glColor3f(0.0f,1.0f,0.0f);  
15:        glVertex3f(0.5f,-0.5f,0.0f);  
16:        glColor3f(0.0f,0.0f,1.0f);  
17:        glVertex3f(0.0f,0.5f,0.0f);  
18:     glEnd();  
19:     glFlush();  
20:     glutSwapBuffers();  
21:     glutPostRedisplay();  
22:  }  


Possíveis melhoras

Não vejo uma possível melhora com a implementação do que foi proposto, porem tem muita coisa a ser feita e melhorada em relação a Classe MyGL que ainda não está completa e não possui um tratamento de exceção adequado.

Dificuldades encontradas

Também não tive problemas com a implementação do trabalho em si. A principal dificuldade encontrada foi com minha adaptação e aprendizado com o C++ que me fez perder muito tempo preparando minha classe MyGL pra que ficasse apta a implementar a mudança de espaços e a implementação da sobrecarga do operador * pois devido algumas dificuldade com a linguagem em si.

Referência bibliográfica s

- Foley, Capítulo 5: "Geometrical Transformations"



terça-feira, 18 de junho de 2013

Rasterização de Primitivas – Computação Gráfica

O trabalho tem como objetivo familiarizar o aluno com o conceito de rasterização de primitivas ( pontos, retas e triângulos) em Computação Gráfica.

Rasterização, é a tarefa de tomar uma imagem descrita em um imagem vetorial e convertê-la em uma imagem raster (pixels ou pontos) para a saída em vídeo ou impressora. Wikipedia.
A atividade proposta era a de implementação de funções de desenhos de pontos, retas e triângulos com interpolação linear das cores dos vértices e dos pontos iniciais e finais das retas. Usando o framework, disponibilizado pelo Prof. Christian Pagot, que simula o acesso à memória de vídeo usando o ponteiro FBptr. Pra isso temos as funções:
  • PutPixel : Desenha um pixel na tela.
  • DrawLine : Desenha uma reta na tela.
  • DrawTriangle : Desenha um triângulo na tela.
Primeiramente, criei três tipos abstratos de dados: tPixel, tCor, tCoordenasBari. E criei duas funções para criação de tPixel e tCor.

typedef struct {
 unsigned char r;
 unsigned char g;
 unsigned char b;
 unsigned char a;
}tCor;

typedef struct {
 int x;
 int y;
 tCor cor;
}tPixel;

typedef struct {
 double v;
 double u;
 double w;
}tCoordenasBari;

tPixel InitPixel( int x, int y, tCor cor )
{
 tPixel p;

 p.x = x;
 p.y = y;
 p.cor = cor;

 return p;
}

tCor InitCor( unsigned char r, unsigned char g, unsigned char b, unsigned char a )
{
 tCor cor;

 cor.r = r;
 cor.g = g;
 cor.b = b;
 cor.a = a;

 return cor;
}


Com todos tipos abstratos criados. Comecei implementando a função PutPixel, pois ela é essencial para implementação das outras duas. Para que possamos desenhar um pixel na tela necessitamos desenhar os valores RGBA na posição na memória de vídeo correspondente àquele pixel. Usando a fórmula de offset consigo saber o espaço correspondente de qualquer pixel na memória.

FBptr[4*x + 4*y*IMAGE_WIDTH]

Obs.: IMAGE_WIDTH é a constante encontrada no arquivo definitions.h do framework que corresponde a largura da tela.

Cade pixel são 4 bytes na memória correspondentes ao valores RGBA, ou seja, precisamos pintar em cada byte o valor de cada componente RGBA do determinado pixel. Com isso temos:


void PutPixel( tPixel *p )
{
int x = p->x;
int y = p->y;

/* Esse bloco leva o FBptr pra area de memoria de 4bytes correspondente
à posição do pixel passado por parametro e insere os valores RGBA do pixel,
onde 4*x + 4*y*IMAGE_WIDTH é a formula usada para levar o FBptr pro lugar correto na memoria. */
FBptr[4*x + 4*y*IMAGE_WIDTH + 0] = p->cor.r;
FBptr[4*x + 4*y*IMAGE_WIDTH + 1] = p->cor.g;
FBptr[4*x + 4*y*IMAGE_WIDTH + 2] = p->cor.b;
FBptr[4*x + 4*y*IMAGE_WIDTH + 3] = p->cor.a;

}


Porém ainda não estava pronto. Precisava-se garantir que a função não iria ter possibilidade de desenhar um pixel fora da tela e possivelmente acessando uma área restrita da memória. Exemplo de um possível erro que poderia ocorrer:



Resolvi esse problema com a inserção da seguinte condição antes de acessar o ponteiro FBptr . Que avalia se o x ou o y está fora do seguinte intervalo 0 <= x <= IMAGE_WIDTH e 0 <= y <= IMAGE_HEIGHT.

/*Se o pixel passado por parâmetro estiver na coordenada x-y fora da tela.
Então ele não é pintado */
if( x < 0 || y < 0 || x > IMAGE_WIDTH || y > IMAGE_HEIGHT )
return;

O mesmo exemplo anterior com a mudança e outro com um pixel dentro dos limites.
PutPixel( -10, -10, 255,255,255,255)
PutPixel (300, 300, 255,255,255,255)

A próxima função a ser implementada foi a DrawLine, que certamente foi a mais complicada devido a necessidade de generalização do algoritmo de Bresenham que desenha retas em apenas 1 octante.
void DrawLine( tPixel pixelI, tPixel pixelF )
{

 /* Algoritmo de Bresenham */
 int dx = pixelF.x - pixelI.x;
 int dy = pixelF.y - pixelI.y;

 int d = 2 * dy - dx;
 int incE = 2 * dy;
 int incNE = 2 * (dy – dx);
 tPixel pixelC = pixelI;

 /* Desenhar o primeiro pixel */
 PutPixel( &pixelC );
 
 /* Desenhar o resto dos pixels até o pixel final */
 while( pixelC.x < pixelF.x )
 {
  if( d <= 0 )
  {
   d += incE;
   pixelC.x++;
  }else{
   d += incNE;
   pixelC.x++;
   pixelC.y++;
  }

  PutPixe( &pixelC );
 }

}


Primeiramente expandi o Bresenham não só ao primeiro octante e sim também para o quarto. Facilmente podemos identificar quando a reta se encontra no quarto octante. Quando o dx ( xFinal – xInicial ) é negativo. Da mesma forma que identifiquei facilmente o problema também solucionei rapidamente. Basta apenas inverter os pixels, o inicial passa a ser o final e o final o inicial. Para facilitar criei a função static SwapPixel.

static void SwapPixel( tPixel *pixelUm, tPixel *pixelDois )
{
tPixel aux;

aux = *pixelUm;
*pixelUm = *pixelDois;
*pixelDois = aux;
}

Que é chamada quando dx for negativo, com isso inserimos o trecho a seguir na função DrawLine após o calculo do dx e do dy.

/* O algoritmo necessita de que o x inicial seja menor que o x final
pois ele vai desenhando da esquerda pra direita.
Então, se x inicial for maior que x final invertemos os pixels. */

if( dx < 0 )
{
SwapPixel( &pixelI, &pixelF );

dx = -dx;
dy = -dy;
}

Com isso já posso desenhar as seguintes retas.

tPixel p;
tPixel p2;

p = InitPixel( 200, 150, InitCor(255,255,255,255));
p2 = InitPixel( 350, 230, InitCor(255,255,255,255));
 
DrawLine( p, p2 );

p = InitPixel( 200, 150, InitCor(0,0,255,255));
p2 = InitPixel( 50, 230, InitCor(0,0,255,255));

DrawLine( p, p2 );

Logo após, modifiquei o algoritmo de forma que desenhasse retas no 5º e 8º octantes. Se analisarmos as retas desses dois octantes vamos notar que elas ao invés de ir incrementando a coordenada Y, na verdade está decrementando. Com isso temos uma solução bem simples. Usei uma variável que determina como será o deslocamento da coordenada Y no algoritmo. Se dy for negativo, ou seja, indo de um Y maior para um Y menor, então mudamos a variável de deslocamento para negativo pra que ela seja somada a coordenada Y toda vez que o pixel a Nordeste for escolhido. Com isso inserimos a seguinte condição após a condição do dx < 0.

/*O algoritmo de Bresenham está implementado para pintar apenas retas que tem o dy positivo.Então, devemos inverter o dy pra positivo quando ele estiver negativo e mudar a maneira que o y ira variar( incrementando ou decrementando ). */

if( dy < 0 )
{
dy = -dy;
deslocaY = -1;
}

E vamos modificar a linha.

pixelC.y++;

Para a seguinte instrução.

pixelC.y += deslocaY;

Como você pode ver, não tive muita dificuldade para a implementação de Bresenham para quatro octantes: o primeiro, quarto, quinto e oitavo. Com isso, temos o seguinte printscreen mostrando as retas nesses octantes.

tPixel p;
tPixel p2;

p = InitPixel( 200, 150, InitCor(255,255,255,255));
p2 = InitPixel( 350, 230, InitCor(255,255,255,255));

DrawLine( p, p2 );

p = InitPixel( 200, 150, InitCor(0,0,255,255));
p2 = InitPixel( 50, 230, InitCor(0,0,255,255));

DrawLine( p, p2 );

p = InitPixel( 200, 150, InitCor(255,0,0,255));
p2 = InitPixel( 350, 70, InitCor(255,0,0,255));

DrawLine( p, p2 );

p = InitPixel( 200, 150, InitCor(0,255,0,255));
p2 = InitPixel( 50, 70, InitCor(0,255,0,255));

DrawLine( p, p2 );

Agora sobrou os outros quatro octantes que possuem as retas com o coeficiente angular maior que 1. Ou seja, o dx é menor que o dy. Pensei em fazer com que a reta passasse a ter o coeficiente menor que 1, com isso basta inverter os eixos de coordenadas, então o dx passa ser maior que o dy. Inicialmente eu inseri mais uma condição antes das do dx < 0 e dy < 0 que inverte os eixos caso o dx seja menor que o dy e muda a variável de avaliação swapEixos para 1, para que o algoritmo possa saber que os eixos foram trocados e na hora de desenhar o pixel apenas destrocar.

Função SwapEixos:
static void SwapEixos( tPixel *pixel )
{
int aux;

aux = pixel->x;
pixel->x = pixel->y;
pixel->y = aux;

}

Primeira implementação:
/* Variavel de certificação para poder inverter novamente os eixos antes de pinta o pixel caso dx < dy */
int swapEixos = 0;

/* Primeiro vamos verificar se a linha tera o coeficiente angular maior que 1.
Já prevendo possiveis modificações nas condições posteriores.
Se tiver, vamos inverter as coordenadas */
if( dx < dy )
{
SwapEixos( &pixelF );
SwapEixos( &pixelI );

int aux = dy;
dy = dx;
dx = aux;

swapEixos = 1;
}

E antes de desenhar os pixels fazemos a destroca. Basta trocar o seguinte trecho em todas as ocorrências do PutPixel( &pixelC );

if( swapEixos )
{
    SwapEixos( &pixelC );
    PutPixel( &pixelC );
    SwapEixos( &pixelC );
}else
    PutPixel( &pixelC );

Já conseguimos desenhar retas no segundo octante e no terceiro como podemos ver na imagem abaixo, podemos ver também que a implementação do jeito que está não abrange o resto dos 2 octantes que faltam.
tPixel p;
tPixel p2;

p = InitPixel( 200, 300, InitCor(255,255,255,255));
p2 = InitPixel( 50, 500, InitCor(255,255,255,255));

DrawLine( p, p2 );

p = InitPixel( 200, 300, InitCor(255,255,255,255));
p2 = InitPixel( 350, 500, InitCor(255,255,255,255));

DrawLine( p, p2 );

p = InitPixel( 200, 300, InitCor(0,0,255,255));
p2 = InitPixel( 50, 100, InitCor(0,0,255,255));

DrawLine( p, p2 );

p = InitPixel( 200, 300, InitCor(0,0,255,255));
p2 = InitPixel( 350, 100, InitCor(0,0,255,255));

DrawLine( p, p2 );

Tentativa ao desenhar novamente o quarto octante, note que o pixel final está na posição 10 na coordenada x.

tPixel p;
tPixel p2;

p = InitPixel( 200, 150, InitCor(0,0,255,255));
p2 = InitPixel( 10, 230, InitCor(0,0,255,255));

DrawLine( p, p2 );

A implementação do jeito que está não funciona pros demais octantes e ainda acaba fazendo com que não se desenhe mais corretamente retas no quarto octante, pois temos que “prever” que depois dos testes de dx < 0 e dy < 0 a reta pode passar a se encaixar no dx < dy e depois de passar pela modificação dos eixos ele ainda pode entrar nas condições de dx < 0 e dy < 0. Então bastamos “prever” que isso ocorrerá. A solução encontrada foi fazer a avaliação de dx < dy usando seus valores absolutos. Portanto, agora temos o algoritmo de desenhar linhas que abrange todos os octantes.

if( fabs(dx) < fabs(dy) )

fabs() - Função que retorna o valor absoluto de qualquer inteiro e se encontra na biblioteca padrão math.h.


tPixel p;
tPixel p2;
p = InitPixel( 200, 300, InitCor(255,255,255,255));
p2 = InitPixel( 50, 500, InitCor(255,255,255,255));
DrawLine( p, p2 );
p = InitPixel( 200, 300, InitCor(255,255,255,255));
p2 = InitPixel( 350, 500, InitCor(255,255,255,255));
DrawLine( p, p2 );
p = InitPixel( 200, 300, InitCor(0,0,255,255));
p2 = InitPixel( 50, 100, InitCor(0,0,255,255));
DrawLine( p, p2 );
p = InitPixel( 200, 300, InitCor(0,0,255,255));
p2 = InitPixel( 350, 100, InitCor(0,0,255,255));
DrawLine( p, p2 );
p = InitPixel( 200, 300, InitCor(255,255,255,255));
p2 = InitPixel( 350, 380, InitCor(255,255,255,255));
DrawLine( p, p2 );
p = InitPixel( 200, 300, InitCor(0,0,255,255));
p2 = InitPixel( 50, 380, InitCor(0,0,255,255));
DrawLine( p, p2 );
p = InitPixel( 200, 300, InitCor(255,0,0,255));
p2 = InitPixel( 350, 220, InitCor(255,0,0,255));
DrawLine( p, p2 );
p = InitPixel( 200, 300, InitCor(0,255,0,255));
p2 = InitPixel( 50, 220, InitCor(0,255,0,255));
DrawLine( p, p2 );
p = InitPixel( 200, 300, InitCor(0,255,0,255));
p2 = InitPixel( 200, 100, InitCor(0,255,0,255));
DrawLine( p, p2 );
p = InitPixel( 200, 300, InitCor(255,0,0,255));
p2 = InitPixel( 200, 500, InitCor(255,0,0,255));
DrawLine( p, p2 );
p = InitPixel( 200, 300, InitCor(0,255,0,255));
p2 = InitPixel( 50, 300, InitCor(0,255,0,255));
DrawLine( p, p2 );
p = InitPixel( 200, 300, InitCor(255,0,0,255));
p2 = InitPixel( 350, 300, InitCor(255,0,0,255));
DrawLine( p, p2 );

A função DrawTriangle tem sua implementação bastante simples precisando apenas desenhar 3 retas usando os 3 vértices passados por parâmetro. Então temos ela da seguinte maneira.

void DrawTriangle( tPixel pUm, tPixel pDois, tPixel pTres )
{
/* Ligamos tres retas aos vertices do triangulo. Pronto */
    DrawLine( pUm, pDois );
    DrawLine( pDois, pTres );
    DrawLine( pTres, pUm );
}


tPixel p;
tPixel p2;
tPixel p3;

p = InitPixel( 150, 150, InitCor(0,0,255,255));
p2 = InitPixel( 150, 500, InitCor(0,0,255,255));
p3 = InitPixel( 500, 500, InitCor(0,0,255,255));

DrawTriangle( p, p2, p3 );

Até agora, já posso desenhar pontos, retas e triângulos. Agora precisa-se implementar a interpolação de cores, variar as cores gradativamente até chegar na cor da outra ponta, que preferi deixar pro final pra facilitar o raciocínio. A interpolação segue uma variação de cada componente RGBA até chegar a cor RGBA do final a partir de uma cor RGBA inicial.

Com isso em mente calculei a variação de cada componente subtraindo o final pelo inicial e dividindo pela quantidade de pixels, que se você pensar bem é o mesmo valor do dx, pois o algoritmo desenha o próximo pixel sempre a leste ou nordeste do anterior e no caso do coeficiente ser maior que 1 o eixo é modificado e o dx continua sendo o número de pixels da reta. Logo após, calculo a cor de cada pixel multiplicando o número de pixels já desenhados e a variação somando com a cor do primeiro pixel. Resumindo temos os seguintes trechos.

Antes do while.
/* Calculo de variação de cor por pixel */
double variacaoR = ((double)pixelF.cor.r - pixelI.cor.r)/dx;
double variacaoG = ((double)pixelF.cor.g - pixelI.cor.g)/dx;
double variacaoB = ((double)pixelF.cor.b - pixelI.cor.b)/dx;
double variacaoA = ((double)pixelF.cor.a – pixelI.cor.a)/dx;
/* Número de pixels pintados até então */
int nPixelPintados = 1;

E antes de desenhar o pixel calculamos a cor correspondente a ele.

Antes do PutPixel dentro do while.
/* A mágica da interpolação acontece aqui
Onde cada pixel vai ter uma o número de pixels ja pintados vezes a variação + o número da cor do primeiro pixel*/
pixelC.cor.r = (nPixelPintados * (char)variacaoR) + pixelI.cor.r;
pixelC.cor.g = (nPixelPintados * (char)variacaoG) + pixelI.cor.g;
pixelC.cor.b = (nPixelPintados * (char)variacaoB) + pixelI.cor.b;
pixelC.cor.a = (nPixelPintados * (char)variacaoA) + pixelI.cor.a;

Após o PutPixel incrementamos o nPixelPintados.
/* Pintou mais um, então incrementamos */
nPixelPintados++;


Essa implementação inicial tinha um pequeno erro, o fato de estar acontecendo a trucagem do valor flutuante antes de fazer todos os cálculos fazem com que retas muitos grandes, mais precisamente, as com dx maior que 255, número máximo que se pode conseguir com a subtração da cor final pela inicial, fazendo com que a variação seja menor que 1 e a trucagem faz com que a cor não varie nunca.

Exemplo:

tPixel p;
tPixel p2;
p = InitPixel( 10, 10, InitCor(0,0,255,255));
p2 = InitPixel( 300, 10, InitCor(255,0,0,255));
DrawLine( p, p2 );

Facilmente resolvido tirando a trucagem para o fim do cálculo com os valores de ponto flutuante. E acrescentando a soma com 0.5 para arredondar o valor antes da trucagem.

pixelC.cor.r = (char) (nPixelPintados * variacaoR + 0.5 ) + pixelI.cor.r;
pixelC.cor.g = (char) (nPixelPintados * variacaoG + 0.5 ) + pixelI.cor.g;
pixelC.cor.b = (char) (nPixelPintados * variacaoB + 0.5 ) + pixelI.cor.b;
pixelC.cor.a = (char) (nPixelPintados * variacaoA + 0.5 ) + pixelI.cor.a;

Está concluído o que foi proposto.
tPixel p;
tPixel p2;
p = InitPixel( 256, 256, InitCor(255,0,0,255));
p2 = InitPixel( 500, 512, InitCor(0,0,255,255));
DrawLine( p, p2 );
p = InitPixel( 256, 256, InitCor(255,0,0,255));
p2 = InitPixel( 12, 512, InitCor(255,255,0,255));
DrawLine( p, p2 );
p = InitPixel( 256, 256, InitCor(255,0,0,255));
p2 = InitPixel( 0, 256, InitCor(0,255,0,255));
DrawLine( p, p2 );
p = InitPixel( 256, 256, InitCor(255,0,0,255));
p2 = InitPixel( 512, 256, InitCor(0,255,255,255));
DrawLine( p, p2 );
p = InitPixel( 256, 256, InitCor(255,0,0,255));
p2 = InitPixel( 12, 0, InitCor(255,0,255,255));
DrawLine( p, p2 );
p = InitPixel( 256, 256, InitCor(255,0,0,255));
p2 = InitPixel( 500, 0, InitCor(0,0,255,255));
DrawLine( p, p2 );
p = InitPixel( 256, 256, InitCor(255,0,0,255));
p2 = InitPixel( 256, 0, InitCor(0,160,255,255));
DrawLine( p, p2 );
p = InitPixel( 256, 256, InitCor(255,0,0,255));
p2 = InitPixel( 256, 512, InitCor(0,255,0,255));
DrawLine( p, p2 );
p = InitPixel( 256, 256, InitCor(255,0,0,255));
p2 = InitPixel( 512, 350, InitCor(30,160,200,255));
DrawLine( p, p2 );
p = InitPixel( 256, 256, InitCor(255,0,0,255));
p2 = InitPixel( 0, 350, InitCor(0,0,0,255));
DrawLine( p, p2 );
p = InitPixel( 256, 256, InitCor(255,0,0,255));
p2 = InitPixel( 512, 152, InitCor(0,255,0,255));
DrawLine( p, p2 );
p = InitPixel( 256, 256, InitCor(255,0,0,255));
p2 = InitPixel( 0, 152, InitCor(255,255,255,255));
DrawLine( p, p2 );


tPixel p;
tPixel p2;
tPixel p3;

p = InitPixel( 150, 150, InitCor(0,0,255,255));
p2 = InitPixel( 150, 500, InitCor(255,0,0,255));
p3 = InitPixel( 500, 500, InitCor(0,255,0,255));

DrawTriangle( p, p2, p3 );

Como terminei tudo que foi proposto em tempo hábil. Aproveitei para implementar alguns extras. São as funções de desenhar triângulos preenchidos usando também interpolação de cores e desenhar quadriláteros com preenchimento interpolado de cores. Temos as seguintes funções:

  • DrawFillTriangle - Desenhar triângulos preenchidos
  • DrawQuadrangle – Desenhar quadriláteros.
  • DrawFillQuadrangle – Desenhar quadriláteros preenchidos.

O meu maior desafio foi a implementação de uma função que desenhasse triângulos preenchidos.

Obs.: Perdi o PrintScreen com o triângulo desenhado incorretamente.

Depois de várias frustrações com alguns algoritmos errados e nada eficientes, achei a solução em um documento que falava sobre Geometria de Triângulos. Com auxilio desse material preenchi triângulos usando as coordenadas baricêntricas do triângulo. São três coordenadas que expressão a distância do ponto em relação a cada vértice que somadas é igual a 1, com isso consegui saber se um determinado ponto se encontra dentro do triângulo. Basta todas as coordenadas serem positivas. Então a função percorre todos os pixels do menor quadrado que pode cobrir todo o triângulo, e vai fazendo o teste se determinado ponto está dentro do triângulo, se sim, basta desenha-lo com a cor sendo a soma das coordenadas baricêntricas multiplicadas pelas componentes do vertice correspondente a ela. Não irei postar a implementação aqui, será visível apenas no código-fonte disponibilizado para download após o fim do prazo de entrega.

Seguindo as seguintes linhas de passos temos o algoritmo para preencher qualquer triângulo.

  • Desenhar um triângulo simples usando o DrawTriangle
  • Guardas as dimensões do menor quadrado que abrange o triângulo.
  • Percorrer todos os pontos do quadrado.
    • Verifica se o ponto pertence ao triângulo, se todas as coordenadas baricêntricas forem positivas.
    • Se sim, calcula cada componente RGBA do ponto somando as coordenadas baricêntricas multiplicadas cada uma pela componente do vertice correspondente.
    • Desenha em seguida.

Parte do código responsavel pela interpolação do triângulo preenchido. Para facilitar o entendimento. Onde pUm, pDois e pTres são as pontas do  triângulo e coordenadas é do tipo tCoordenasBari.
p.cor.r = (char)((coordenadas.v * pUm.cor.r + coordenadas.u * pDois.cor.r + coordenadas.w * pTres.cor.r) + 0.5);
p.cor.g = (char)((coordenadas.v * pUm.cor.g + coordenadas.u * pDois.cor.g + coordenadas.w * pTres.cor.g) + 0.5);
p.cor.b = (char)((coordenadas.v * pUm.cor.b + coordenadas.u * pDois.cor.b + coordenadas.w * pTres.cor.b) + 0.5);
p.cor.a = (char)((coordenadas.v * pUm.cor.a + coordenadas.u * pDois.cor.a + coordenadas.w * pTres.cor.a) + 0.5);


tPixel p;
tPixel p2;
tPixel p3;

p = InitPixel( 150, 150, InitCor(0,0,255,255));
p2 = InitPixel( 150, 500, InitCor(255,0,0,255));
p3 = InitPixel( 500, 500, InitCor(0,255,0,255));

DrawFillTriangle( p, p2, p3 );

Para desenhar quadriláteros basta ligar quatro retas com os vértices do mesmo, mas de forma que nenhuma reta cruze a outra.

Seguindo o algoritmo:

  • Ordenar os pontos de forma que eles não se cruzem.
  • Ligar os pontos usando DrawLine.


tPixel p;
tPixel p2;
tPixel p3;
tPixel p4;

p = InitPixel( 150, 150, InitCor(0,0,255,255));
p2 = InitPixel( 50, 500, InitCor(255,0,0,255));
p3 = InitPixel( 400, 500, InitCor(0,255,0,255));
p4 = InitPixel( 500, 150, InitCor(255,255,0,255));

DrawQuadrangle( p, p2, p3, p4 );

Para a função DrawFillQuadrangle, necessitamos apenas dividir o quadriláteros em dois, formando dois triângulos e usar a função DrawFillTriangle para desenha-los.

Algoritmo:

  • Ordenar os pontos de forma que eles não se cruzem.
  • Escolher dois pontos de forma que divide o quadrilátero em dois.
  • Chama a função DrawFillTriangle para desenhar os dois triângulos.

tPixel p;
tPixel p2;
tPixel p3;
tPixel p4;

p = InitPixel( 150, 150, InitCor(0,0,255,255));
p2 = InitPixel( 50, 500, InitCor(255,0,0,255));
p3 = InitPixel( 400, 500, InitCor(0,255,0,255));
p4 = InitPixel( 500, 150, InitCor(255,255,0,255));

DrawFillQuadrangle( p, p2, p3, p4 );

Possíveis melhoras

Acredito que poderia aumentar a eficiência simplificando algumas instruções no desenho de retas e na interpolação de cores, trabalhar mais no preenchimento de triângulos, melhorando também a sua eficiência. Resumindo, as possíveis melhoras seriam na eficiência.

Dificuldades encontradas

Sem duvida, as maiores dificuldades encontradas foi a generalização o algoritmo de Bresenham para desenhar retas com o coeficiente angular maior do que 1 e o preenchimento de triângulos.

Referência bibliográficas

quinta-feira, 13 de junho de 2013

segunda-feira, 10 de junho de 2013

Prints Screens - T1


Primeiros prints do primeiro trabalho do curso de Computação Gráfica da Universidade Federal da Paraiba.