[ICG] Trabalho 2: Conversão do espaço canônico para espaço de tela
Autor: Gutierrez PS | Publicado: 2013-07-10T22:27:00-03:00


O primeiro trabalho da disciplina pediu para implementar um algoritmo que, dada uma coordenada no espaço de tela, fosse colocada na memória de vídeo para, em seguida, ser exibida na tela. Entretanto, não é prático descrever funções e objetos neste espaço, pois frequentemente possuem dimensões diferentes da tela.


Deste modo, a descrição mais adequada é feita no espaço canônico, o passo anterior ao da rasterização. Neste espaço, todos os vetores estão descritos em três dimensões, com o valor dos seus componentes dentro do intervalo [-1, 1]. Portanto, o segundo trabalho da disciplina consiste em implementar a conversão deste espaço canônico para o espaço da tela. Neste trabalho, serão consideradas apenas as coordenadas x&nbsp;e y&nbsp;dos vetores.


Processo de conversão

Um objeto no espaço canônico, como descrito anteriormente, possui vértices cujas componentes estão compreendidas entre o intervalo [-1, 1], como na figura abaixo.



Objeto descrito no espaço canônico



O primeiro passo é inverter as coordenadas y, pois o sistema de coordenadas de tela possui o eixo y&nbsp;invertido em relação ao espaço canônico. Em seguida, translada-se a área para o quadrante positivo, o que resultará em um quadrado de lado 2. Depois, aplica-se uma escala para que o quadrado tenha lado de comprimento 1.



Objeto transladado, escalado e com a coordenada y&nbsp;invertida



Agora, basta escalar este quadrado para que o comprimento dos lados seja igual ao tamanho da tela de destino. Suponhamos que a tela de destino tenha uma largura W&nbsp;e altura H. Assim, o valor máximo que uma coordenada pode assumir, na tela, é (W-1, H-1). Aplicando a escala, e observando o objeto já no espaço de tela, temos a figura abaixo.



Objeto no espaço de tela, após todas as transformações



Observa-se aqui o propósito da reflexão executada no primeiro passo. Quando o objeto passa a ser visto a partir do espaço de tela, suas coordenadas y&nbsp;são espelhadas naturalmente.


Cada transformação apresentada pode ser representada por uma matriz. Em seguida, pode-se "juntar" todas as matrizes em uma só, multiplicando-as na mesma ordem em que as operações devem ser aplicadas. Temos então as matrizes abaixo.





A matriz resultante da multiplicação contém todas as transformações apresentadas. Daí, basta multiplicar esta matriz por um vetor representado no espaço canônico, e o resultado será um vetor correspondente no espaço de tela.





Onde W&nbsp;e H&nbsp;são as dimensões da tela de destino. A coordenada z'&nbsp;permanece por questões de referência de profundidade.

Modelagem

Para facilitar a multiplicação entre as matrizes e vetores, e também por sugestão do próprio professor, represento as matrizes e os vetores como objetos (em C++), que contém a matriz/vetor e uma sobrecarga do operador da multiplicação.

class Vector4 {
    public:
        float m[4];
        unsigned char color[4];
        Vector4();
        Vector4(float, float, float, float);
        Vector4(float, float, float, float, unsigned char,
                unsigned char, unsigned char, unsigned char);
};
    Vector4::Vector4() {        // default values
        for(int i = 0; i &lt; 4; i++) {
            m[i] = 0;
            color[i] = 255;
        }
    };
    // only coordinates
    Vector4::Vector4(float a, float b, float c, float d) {
        m[0] = a;
        m[1] = b;
        m[2] = c;
        m[3] = d;
        for(int i = 0; i &lt; 4; i++) color[i] = 255;
    }
    // coordinates and color
    Vector4::Vector4(float a, float b, float c, float d,
                     unsigned char red, unsigned char green,
                     unsigned char blue, unsigned char alpha) {
        m[0] = a;
        m[1] = b;
        m[2] = c;
        m[3] = d;
        color[0] = red;
        color[1] = green;
        color[2] = blue;
        color[3] = alpha;
    };
    

class Matrix4x4 {
    public:
        float m[4][4];
        Matrix4x4();
        Matrix4x4(float [4][4]);
        Matrix4x4 operator * (Matrix4x4);
        Vector4 operator * (Vector4);
};
    Matrix4x4::Matrix4x4() {    // default constructor
        for(int i = 0; i &lt; 4; i++)
            for(int j = 0; j &lt; 4; j++)
                m[i][j] = 0.0f;
    };
    Matrix4x4::Matrix4x4(float init[4][4]) {    // custom constructor
        for(int i = 0; i &lt; 4; i++)
            for(int j = 0; j &lt; 4; j++)
                m[i][j] = init[i][j];
    };
    Matrix4x4 Matrix4x4::operator * (Matrix4x4 b) {  // matrix*matrix
        Matrix4x4 res;
        for(int i = 0; i &lt; 4; i++) {
            for(int j = 0; j &lt; 4; j++) {
                res.m[i][j] = 0;
                for(int k = 0; k &lt; 4; k++)
                    res.m[i][j] += m[i][k]*b.m[k][j];
            }
        }
        return (res);
    };
    Vector4 Matrix4x4::operator * (Vector4 b) {     // matrix*vector
        Vector4 res;

        for(int i = 0; i &lt; 4; i++) {
            res.m[i] = 0;
            res.color[i] = b.color[i];
            for(int k = 0; k &lt; 4; k++)
                res.m[i] += m[i][k]*b.m[k];
        }
        
        return(res);
    };



Na classe Matrix4x4 estão definidas as rotinas de multiplicação entre outra matriz e entre um vetor. Em relação ao trabalho anterior, resolvi modelar os pixels como objetos também. A função PutPixel() está agora incorporada na classe Pixel, como a função put().

class Pixel {
    public:
        unsigned short int x, y;
        unsigned char color[4];
        Pixel(unsigned short int, unsigned short int,
                  unsigned char, unsigned char, unsigned char,
                  unsigned char);
        void put();
};
    Pixel::Pixel(unsigned short int inX, unsigned short int inY,
                      unsigned char r, unsigned char g, unsigned char b,
                      unsigned char a) {
        x = inX;
        y = inY;
        color[0] = r;
        color[1] = g;
        color[2] = b;
        color[3] = a;
    };
    void Pixel::put() {
        int offset = 4*(x + IMAGE_WIDTH*y);
 
        if( (x &lt; IMAGE_WIDTH) &amp;&amp; (y &lt; IMAGE_HEIGHT) ) {
          FBptr[offset + RED] = color[RED];
          FBptr[offset + GREEN] = color[GREEN];
          FBptr[offset + BLUE] = color[BLUE];
          FBptr[offset + ALPHA] = color[ALPHA];
        }
    };



Dito isto, a tarefa da função que converte as coordenadas do espaço canônico para o espaço de tela nada mais é do que receber o vetor daquele espaço, multiplicar pela matriz de transformação, e retornar as coordenadas do pixel na tela. Por comodidade, o objeto Vector4  armazena as informações de cor daquele vértice, e que são passadas adiante quando há uma multiplicação entre uma matriz e um vetor.

Pixel canonicalToScreen(Vector4 coord) {
    float temp[4][4] = {
            { (IMAGE_WIDTH - 1)/2.0f, 0, 0, (IMAGE_WIDTH-1)/2.0f}, 
            { 0, -(IMAGE_HEIGHT - 1)/2.0f, 0, (IMAGE_HEIGHT-1)/2.0f},
            {0, 0, 1/2.0f, 0},
            {0, 0, 0, 1}
        };
    
    Matrix4x4 tMatrix(temp);    // transformation matrix
    
    Vector4 buf = tMatrix * coord;      // canonical -&gt; screen space

    // composing pixel
    Pixel pix ((int)buf.m[0], (int)buf.m[1], 255, 255, 255, 255);
    
    return pix;
}



Onde temp é a matriz de conversão, calculada anteriormente. Dados, então, três vértices no espaço canônico

Vector4 a (-0.5f, -0.5f, 0, 1);
Vector4 b ( 0.5f, -0.5f, 0, 1);
Vector4 c ( 0.0f,  0.5f, 0, 1);


Para mostrá-los na tela, converte-se para o espaço de tela:

Pixel p0 = canonicalToScreen(a);
Pixel p1 = canonicalToScreen(b);
Pixel p2 = canonicalToScreen(c);


E então, adotando-os como vértices de um triângulo,

DrawTriangle(p0, p1, p2);



Obtém-se a figura a seguir (lado esquerdo). O lado direito apresenta o código de referência do trabalho, onde são utilizadas as funções do OpenGL.



À esquerda, DrawTriangle(p0, p1, p2) utilizando o método de conversão desenvolvido. À direita, a imagem de referência