12. Desenhando com WebGL usando transformações

Em nossa introdução à programação com shaders usando WebGL desenhamos apenas formas básicas para introduzir os princípios de programação usando shaders. Para desenhar cenas mais complexas vamos fazer uso de transformações, como já começamos a explorar usando o canvas 2D do HTML5. Nessa aula, vamos continuar usando exemplos no espaço 2D para aplicação e composição de transformações afins na forma matricial, como introduzimos na aula anterior.

Você deve se lembrar que, para gerar desenhos de objetos, em geral, é conveniente desenhar usando coordenadas centradas no objeto, por exemplo, um cubo ou uma esfera com seus vértices ao redor da origem. A ideia é, assim como em várias outras áreas da computação, utilizar modelos disponíveis em bibliotecas contendo vários objetos complexos e utilizar esses objetos para compor cenas aplicando transformações adequadas a cada objeto. Assim, ao invés de “desenhar uma cena”, a geração de desenhos em computação gráfica se parece mais como um processo de composição de uma cena em 3D, como a montagem de um cenário fotográfico real antes de tirarmos uma foto. Isso permite também reutilizar um mesmo objeto e simplesmente “transformá-lo” para vários lugares da cena, como usar o mesmo modelo de automóvel em lugares diferentes em uma cena de trânsito, ou usar um modelo de árvore na composição de uma floresta ou parque.

12.1. Transformações afins: revisão

As transformações lineares e afins são essenciais na computação gráfica. Lembre-se que uma transformação linear é um mapeamento em um espaço vetorial que preserva combinações lineares. Tais transformações incluem rotações, escalas, cisalhamento (que “estica” retângulos em paralelogramos) e suas combinações.

Da mesma forma, as transformações afins são transformações que preservam as combinações afins. Por exemplo, se \(P\) e \(Q\) são dois pontos e \(m\) é seu ponto médio, e \(\mathbf{T}\) é uma transformação afim, então o ponto médio de \(\mathbf{T}(p)\) e \(\mathbf{T}(q)\) é \(\mathbf{T}(m)\).

Uma propriedade importante das transformações afins é que elas preservam linhas retas e paralelismo, embora não preservem ângulos. Vimos na aula passada como cada transformação básica pode ser representada de forma matricial. Hoje vamos ver que como essas transformações básicas podem ser compostas por meio de multiplicação de matrizes para realizar transformações complexas.

Vamos começar vendo como realizar a animação de bolas transferindo o cálculo de translações para o vertex shader.

12.2. Animação de círculos usando translação no vertex shader

Nesse exemplo vamos modificar o código descrito na aula 8 - Interação e animação com WebGL que ilustrou a animação de bolas usando WebGL recalculando a posição dos vértices para gerar cada imagem. Por exemplo, o trecho de código

// desenha vertices
gPosicoes = [];
for (let i = 0; i < gObjetos.length; i++)
  gObjetos[i].atualize(delta);

mostra que o buffer gPosicoes é limpo para que as novas posições sejam recalculadas e recarregadas no buffer durante a atualização de cada objeto. Depois de recalcular os vértices na CPU, esse novo array precisa ser enviado a GPU para serem desenhados.

Uma alternativa seria, caso a gente possa representar os vértices de uma forma normalizada, por exemplo, centrados ao redor da origem, então podemos manter o buffer de vértices na GPU e, ao desenhar, pedir para o vertex shader transladar o centro para a posição desejada. A CPU então apenas precisa atualizar a nova posição de cada bola, e passar esse único ponto para a GPU redesenhar toda a bola usando os mesmos vértices normalizados!

Vamos fazer isso introduzindo o uniforme uTranslation no código do vertex shader, como no trecho a seguir:

var gVertexShaderSrc = `#version 300 es

// aPosition é um buffer de entrada
in vec2 aPosition;
uniform vec2 uResolution;
uniform vec2 uTranslation;
in vec4 aColor;  // buffer com a cor de cada vértice
out vec4 vColor; // varying -> passado ao fShader

void main() {
    vec2 escala1 = (aPosition + uTranslation) / uResolution;
    vec2 escala2 = escala1 * 2.0;
    vec2 clipSpace = escala2 - 1.0;

    gl_Position = vec4(clipSpace, 0, 1);
    vColor = aColor;
}
`;

Observe que a GPU calcula a translação modificando a posição de cada vértice na função main pelo comando (aPosition + uTranslation). Isso poupa um pouco do trabalho da CPU.

Com isso, a função de atualização dos vértices agora só precisa atualizar o centro do disco definido em this.pos e sua velocidade em this.vel, como abaixo:

this.atualize = function (delta) {
    this.pos = add(this.pos, mult(delta, this.vel));
    let x, y;
    let vx, vy;
    [x, y] = this.pos;
    [vx, vy] = this.vel;

    // bateu? Altere o trecho abaixo para considerar o raio!
    if (x < 0) { x = -x; vx = -vx; };
    if (y < 0) { y = -y; vy = -vy; };
    if (x >= gCanvas.width) { x = gCanvas.width; vx = -vx; };
    if (y >= gCanvas.height) { y = gCanvas.height; vy = -vy; };
    // console.log(x, y, vx, vy);
    this.pos = vec2(x, y);
    this.vel = vec2(vx, vy);
}

Finalmente, a função de desenho precisa ser modificada para que, ao invés de atualizar os vértices, apenas modifique o uniforme uTranslate com a translação de cada objeto, como:

function desenhe() {
    // calcula a diferença de tempo da última atualização
    let now = Date.now();
    let delta = (now - gUltimoT) / 1000; // velocidade em segundos
    gUltimoT = now;

    // limpa o canvas
    gl.clear(gl.COLOR_BUFFER_BIT);

    // desenha posicoes, mas mantem os vértices
    for (let i = 0; i < gObjetos.length; i++) {
        let obj = gObjetos[i];
        obj.atualize(delta);
        gl.uniform2f(gShader.uTranslation, obj.pos[0], obj.pos[1]);
        gl.drawArrays(gl.TRIANGLES, 3*obj.nv*i, 3*obj.nv);
    }

    window.requestAnimationFrame(desenhe);
}

O código completo desse exemplo, disponível abaixo, pode ser visto no JSitor:

12.3. Mais animação: usando escala

Vamos dar mais um passo para transferir mais trabalho para a GPU e simplificar o trabalho da CPU.

A função aproximeDisco() usada anteriormente desenha um disco de raio arbitrário (qualquer tamanho). Considere agora que essa função é capaz de aproximar um disco de raio unitário apenas, ao redor da origem, como abaixo:

function aproximeDisco(ref=4) {
    let raio = 1.0;
    // primeiro um quadrado ao redor da origem
    let vertices = [
        vec2(raio, 0),
        vec2(0, raio),
        vec2(-raio, 0),
        vec2(0, -raio),
    ];

    // refinamento: adiciona 1 vértice em cada lado
    for (let i=1; i<ref; i++) {
        let novo = [];
        let nv = vertices.length;
        for (let j = 0; j<nv; j++) {
            novo.push(vertices[j]);
            let k = (j+1)%nv;
            let v0 = vertices[j];
            let v1 = vertices[k];
            let m  = mix(v0, v1, 0.5);

            let s = raio/length(m);
            m = mult(s, m)
            novo.push( m );
        }
        vertices = novo;
    }
    return vertices;
};

Nesse caso, podemos utilizar uma transformação de escala para esticar o disco até o tamanho desejado. Inclusive podemos agora esticar o disco de forma não uniforme.

Antes de continuar, copie o código do exemplo anterior, fazendo um fork no JSitor e, utilizando essa nova função aproximeDisco(), modifique o código restante para fazer cada disco ser renderizado usando transformações de escala realizadas pelo vertex shader.

Pausa para pensar …

Antes de prosseguir sua leitura, procure imaginar como seria o novo vertex shader. Em particular, como aplicar a transformação de escala ao buffer aPosition.

Para incluir uma transformação de escala não uniforme no vertex shader podemos utilizar mais um uniforme do tipo vec2, uScale, como:

var gVertexShaderSrc = `#version 300 es

// aPosition é um buffer de entrada
in vec2 aPosition;
uniform vec2 uResolution;
uniform vec2 uTranslation;
uniform vec2 uScale;

in vec4 aColor;  // buffer com a cor de cada vértice
out vec4 vColor; // varying -> passado ao fShader

void main() {
    vec2 sPos = aPosition * uScale;

    vec2 escala1 = (sPos + uTranslation) / uResolution;
    vec2 escala2 = escala1 * 2.0;
    vec2 clipSpace = escala2 - 1.0;

    gl_Position = vec4(clipSpace, 0, 1);
    vColor = aColor;
}
`;

Observe que a transformação de escala deve ser aplicada antes da translação.

Após registrar o novo uniforme na criação dos shaders, além de arrumar alguns outros detalhes nas chamadas dessas funções, a nova função de desenho precisa passar a escala:

function desenhe() {
    // atualiza o relógio
    let now = Date.now();
    let delta = (now - gUltimoT) / 1000;
    gUltimoT = now;
    // limpa
    gl.clear(gl.COLOR_BUFFER_BIT);

    // atualiza uniformes sem modificar os vértices
    for (let i = 0; i < gObjetos.length; i++) {
        let obj = gObjetos[i];
        obj.atualize(delta);
        gl.uniform2f(gShader.uTranslation, obj.pos[0], obj.pos[1]);
        gl.uniform2f(gShader.uScale, obj.raio * obj.sx, obj.raio * obj.sy);
        gl.drawArrays(gl.TRIANGLES, obj.nv * i * 3, 3 * obj.nv);
    }
    window.requestAnimationFrame(desenhe);
}

Observe que nesse trecho de código, o fator de escala aplicado é proporcional ao raio, mas também uma escala (sx, sy) independente em cada eixo que permite distorcer o disco como um ovo ou elipse. O código completo desse exemplo pode ser acessado no JSitor.

12.4. Mais animação: usando rotação

Desejamos agora simular um efeito de rotação do disco, introduzindo uma velocidade de rotação vrz e um ângulo theta que corresponde à orientação do disco. Vamos modificar o vertex shader para receber um uniform float uTheta que define um ângulo de rotação do disco.

Como vimos na aula anterior, para aplicar uma rotação por uTheta ao redor da origem sobre um ponto (x, y), podemos aplicar a seguinte formula:

\(novo_x = x * cos( uTheta ) + y * sin( uTheta )\)

\(novo_y = y * cos( uTheta ) - x * sin( uTheta )\)

Novamente, faça um fork do código do exemplo anterior para incluir rotação na nossa animação dos discos.

Outra pausa para pensar …

Antes de prosseguir sua leitura, procure imaginar como seria o novo vertex shader. Em particular, como aplicar a transformação de rotação a cada vértice do buffer aPosition.

O seguinte trecho de código define nosso vertex shader que recebe essas transformações de translação, escala e rotação e as aplica sobre cada vértice em aPosition:

var gVertexShaderSrc = `#version 300 es

// aPosition é um buffer de entrada
in vec2 aPosition;
uniform vec2 uResolution;
uniform vec2 uTranslation;
uniform vec2 uScale;
uniform float uTheta;

in vec4 aColor;  // buffer com a cor de cada vértice
out vec4 vColor; // varying -> passado ao fShader

void main() {
    float s = sin(uTheta);
    float c = cos(uTheta);
    vec2 sPos = aPosition * uScale;
    vec2 rPos = vec2(
        sPos.x * c + sPos.y * s,
        sPos.y * c - sPos.x * s );

    vec2 escala1 = (rPos + uTranslation) / uResolution;
    vec2 escala2 = escala1 * 2.0;
    vec2 clipSpace = escala2 - 1.0;

    gl_Position = vec4(clipSpace, 0, 1);
    vColor = aColor;
}
`;

Dessa vez, o vertex shader recebe um uniform float uTheta e usa suas próprias funções sin() e cos() para calcular a rotação de cada vértice. Observe nesse trecho de código que a rotação está sendo aplicada após o escalonamento e antes da translação. Procure imaginar o que acontece com o desenhe se essa ordem fosse diferente. Por exemplo, o que aconteceria se a rotação fosse aplicada antes do escalonamento? Experimente!!

O restante do código é semelhante ao anterior. A velocidade angular precisa ser definida e armazenada em cada disco, além de ser atualizada também na geração de cada novo desenho. Observe que escolhemos mudar a direção de rotação toda vez que o disco bate em uma das paredes do canvas. O código completo desse exemplo é mostrado abaixo e pode também ser acessado no JSitor.

12.5. Matrizes de transformação no WebGL

Vimos na aula anterior que essas transformações básicas de translação, rotação, escala etc., podem ser representadas de forma matricial de forma muito mais compacta e elegante que a apresentada nos exemplos anteriores. Tipicamente, vamos utilizar matrizes de transformação para realizar as seguintes operações:

  • Movimentação de objetos: como acabamos de ver, mas combinando translações, rotações e outras transformações em um matriz.
  • Mudança de coordenadas: a mudança é usada quando objetos armazenados em relação a um sistema de referência precisam ser acessados em um outro sistema. Um caso importante disso é o de mapear objetos armazenados em um sistema de coordenadas padrão para um sistema de coordenadas associado à câmera (ou observador).
  • Projeção: Tais transformações são usadas para projetar objetos do espaço de desenho normalizado para a viewport e mapear o viewport para o canvas (ou janela). Veremos que as projeções perspectiva são mais gerais do que as transformações afins, pois podem não preservar o paralelismo.
  • Mapeamento entre superfícies: isso é útil quando texturas são mapeadas para superfícies de objetos como parte do processo de mapeamento de textura.

A maneira de aplicação dessas transformações no WebGL é similar ao modelo de pen-plotter, onde os atributos do desenho (com cor e grossura da pena) precisam ser definidos antes do comando para desenhar. Assim as transformações também precisam ser definidas antes e, ao desenhar, o atual estado de transformações é aplicado automaticamente a todos os objetos desenhados. Nos exemplos anteriores, isso foi realizado atribuindo valores aos uniformes uTranslation, uTheta e uScale antes de desenhar cada disco.

Como as transformações são usadas para diferentes propósitos, programas gráficos usando o WebGL costumam trabalhar com três conjuntos de matrizes para organizar os seguintes tipos de transformação:

  • Matriz Modelview: Usada para transformar objetos na cena e para alterar as coordenadas em um forma mais fácil de trabalhar para o WebGL. É tipicamente usada para as tarefas de movimentação de objetos e mudanças de coordenadas. Pense em usar a matriz modelview para configurar a cena, definindo as posições, tamanhos e orientações dos objetos na cena. Nos exemplos anteriores, essa matriz combinaria as transformações para animação dos discos.
  • Matriz de projeção: lida com projeções paralela e perspectiva. Como o próprio nome indica, é utilizada para tarefas de projeção. Pense em usar a matriz de projeção para configurar a câmera, como se você fosse tirar uma foto da cena. Nos exemplos anteriores, poderíamos incluir na matriz de projeção a transformação das coordenadas normalizadas para o canvas.
  • Matriz de textura: Isso é usado para especificar como as texturas são mapeadas em objetos. Vamos adiar a discussão dessa matriz para o final do semestre, quando tratarmos de mapeamento de texturas.

Como já vimos, há ainda mais uma transformação que não é tratada por essas matrizes. Esta é a transformação que mapeia o espaço normalizado para uma região do canvas, definido por gl.viewport(), onde gl é o contexto gráfico webgl2 do canvas do HTML5.

É essencial entender como trabalhar e manipular essas matrizes de transformação para entender como os programas e sistemas gráficos que trabalham no modo imediato funcionam.

12.6. Desenhando no WebGL usando matrizes

Como último exemplo vamos agora juntar todas as transformações na forma de uma única matriz.

Observe no exemplo com rotação que o cálculo do seno e cosseno é feito para cada vértice. Isso pode ser computacionalmente custoso, principalmente para objetos complexos com um grande número de vértices. Para balancear melhor essa carga computacional, pode ser conveniente passar apenas uma matriz de transformação para o vertex shader que combine todas as transformações.

Para facilitar o processamento dessas matrizes utilizaremos a biblioteca MVnew.js, proveniente do livro Interactive Computer Graphics, de Angel e Shneier, que também oferece operações com vetores.

Vamos também começar a usar pontos e vetores em 3D com coordenadas homogêneas. Assim, cada ponto ou vetor é do tipo vec4. Para trabalhar no plano xy, vamos considerar a coordenada z=0. Isso implica também que as nossas matrizes de transformação serão do tipo mat4, com \(4\times4\) elementos. Essa forma também é mais próxima a representação padrão do GLSL.

O trecho de código abaixo ilustra o vertex shader usando uma única matrix de transformação, passada à GPU pelo uniform mat4 uMatrix.

gVertexShaderSrc = `#version 300 es

// aPosition é um buffer de entrada
in vec2 aPosition;
uniform mat4 uMatrix;

in vec4 aColor;  // buffer com a cor de cada vértice
out vec4 vColor; // varying -> passado ao fShader

void main() {
    gl_Position = vec4( uMatrix * vec4(aPosition,0,1) );
    vColor = aColor;
}
`;

Observe que o GLSL possui o tipo mat4 (e mat3) que facilita a manipulação de matrizes. O uniforme uMatrix é uma matriz que combina a matriz de projeção e a matriz modelView. A matriz de projeção mapeia pontos no canvas para o sistema de coordenadas normalizadas e pode ser definida como:

let w = gCanvas.width;
let h = gCanvas.height;
let projection = mat4(
    2/w,    0,  0, -1,
      0, -2/h,  0,  1,
      0,     0,  1, 0,
      0,     0,  0, 1
    );

Observe que projection corresponde a representação matricial da transformação que mapeia um ponto no canvas para um ponto no espaço normalizado de cantos [-1,-1] e [+1,+1]. Por exemplo, calcule o resultado da multiplicação dessa matriz pelos pontos:

  • vec4(w/2, h/2, 0, 1), no centro do canvas;
  • vec4(0, 0, 0, 1), no canto superior esquerdo do canvas; e
  • vec4(w, h, 0, 1), no canto inferior direito do canvas.

Para fazer a multiplicação, considere cada ponto como um vetor coluna.

Representação em ordem de colunas

Na matemática e na computação, estamos acostumados a representar matrizes ordenadas por linhas, ou seja, na matriz [ [1,2], [3,4] ] o elemento [1,2] corresponde a 1a linha. O OpenGL (e o WegGL) adota uma representação de matrizes ordenadas por colunas. Nesse caso, o elemento [1, 2], corresponde à primeira coluna.

Usando a biblioteca MVnew.js, ao criar um mat4 como nesse exemplo, passamos os elementos na ordem que estamos acostumados (por linhas) e a própria biblioteca transforma para a representação do WebGL.

Resumindo, ao trabalhar com elementos do tipo vec e mat da biblioteca, você não precisa se preocupar com essa ordem. Mas se você utilizar arrays do JavaScript, então deve tomar cuidado!

O seguinte trecho calcula a matriz de transformação modelView:

var modelView = translate( obj.pos[0], obj.pos[1], 0 );
modelView = mult(modelView, rotateZ(obj.theta));
modelView = mult(modelView, scale(obj.sx*obj.raio, obj.sy*obj.raio, 1));
// combina projection e modelveiw
var uMatrix = mult(projection, modelView);

Assim como no exercício que fizemos usando transformações no canvas, a ordem para compor as transformações na matriz parece invertida. Mas por se tratar de multiplicação de matrizes, observe no código do vertex shader que

\(gl_Position = uMatrix * aPosition\)

Podemos decompor uMatrix como: \(projection * T * R * S * Position\), onde T, R e S correspondem as matrizes de translação, rotação e escala, respectivamente. Assim, a primeira transformação aplicada a um vértice é a de escala, seguida por rotação, translação e projeção, que é a ordem mais comum para pensar nesse problema.

O código completo desse exemplo, disponível abaixo, pode ser visto no JSitor:

12.7. Desenhando objetos diferentes usando VAO (opcional)

Um objeto VAO (Vertex Array Object) é um recurso oferecido pelo WebGL 2.0 que facilita a organização dos buffers e seus atributos usados para desenhar.

Uma aplicação típica de VAOs é quando temos objetos muitos distintos ou que, por algum motivo, escolhemos desenhar separadamente, usando um outro conjunto de buffers.

Para dar um exemplo de aplicação de VAOs, vamos estender o exemplo anterior incluindo também quadrados em nossa animação.

Um quadrado de lado unitário centrado na origem do sistema de coordenadas normalizado padrão do WebGL pode ser desenhado nos arrays gPosicoesQuads e gCoresQuads como ilustrado no trecho abaixo:

function desenheQuad(cor) {
    let vt = [  // quadrado de lado 1
        vec2( 0.5,  0.5),
        vec2(-0.5,  0.5),
        vec2(-0.5, -0.5),
        vec2( 0.5, -0.5)
        ];
    let i, j, k;
    [i, j, k] = [0,1,2]
    gPosicoesQuads.push( vt[i] );
    gPosicoesQuads.push( vt[j] );
    gPosicoesQuads.push( vt[k] );

    [i, j, k] = [0,2,3]
    gPosicoesQuads.push( vt[i] );
    gPosicoesQuads.push( vt[j] );
    gPosicoesQuads.push( vt[k] );

    for (let i=0; i<6; i++)
        gCoresQuads.push(cor);
}

Um objeto Quad pode ser representado como:

function Quad (x, y, vx, vy, vrz, sx, sy, cor) {
    desenheQuad(cor);
    this.pos = vec2(x, y);   // posição
    this.vel = vec2(vx, vy); // velocidade
    this.theta = 0;          // orientação
    this.vrz = vrz;          // velocidade de rotação
    this.sx = sx;            // escala em x
    this.sy = sy;            // escala em y
}

Apesar de podermos usar os mesmos shaders do exemplo de animação com discos, desejamos criar buffers de posições e cores distintos para armazenar os quadrados, para desenhar discos e quadrados usando os mesmos shaders.

Uma solução é criar VAOs para cada conjunto de buffers, como ilustrado no trecho a seguir.

Observe que a parte de código para criação dos buffers para armazenar as cores e posições dos discos é muito semelhante ao código usado para armazenar as informações dos quadrados. A novidade desse trecho é o uso de VAOs, que são criados usando gl.createVertexArray() e habilitados por gl.bindVertexArray(). Uma vez habilitado, os buffers e atributos criados em seguida são associados a esse VAO.

Observe que, ao final do uso de um VAO, é considerado uma boa prática “limpar” o VAO fazendo gl.bindVertexArray(null).

O uso de VAO facilita muito o desenho. Agora que os buffers foram criados e carregados, a função de desenho dos discos apenas precisa chamar gl.bindVertexArray(gShader.discosVAO) antes de desenhar, assim como a função de desenho dos quadrados só precisa chamar gl.bindVertexArray(gShader.quadsVAO).

Por exemplo, podemos usar a seguinte função de desenho

function desenheQuads(delta, projection) {
    // atualiza e desenha quads
    gl.bindVertexArray(gShader.quadsVAO);
    for (let i=0; i<gQuads.length; i++) {
        let obj = gQuads[i];
        atualize(obj, delta);

        // Calcula a matriz modelView
        var modelView = translate( obj.pos[0], obj.pos[1], 0 );
        modelView = mult(modelView, rotateZ(obj.theta));
        modelView = mult(modelView, scale(obj.sx, obj.sy, 1));
        // combina projection e modelveiw
        var uMatrix = mult(projection, modelView);
        // carrega na GPU
        gl.uniformMatrix4fv(gShader.uMatrix, false, flatten(uMatrix));
        // desenhe
        gl.drawArrays(gl.TRIANGLES, 6*i, 6);
    }
}

e outra semelhante chamada desenheDiscos() para desenhar os discos. A função para desenhar discos e quadrados com animação seria

function desenhe() {
    // atualiza o relógio
    let now = Date.now();
    let delta = (now - gUltimoT)/1000;
    gUltimoT = now;

    // limpa o canvas
    gl.clear( gl.COLOR_BUFFER_BIT );

    // cria a matriz de projeção - pode ser feita uma única vez
    let w = gCanvas.width;
    let h = gCanvas.height;
    let projection = mat4(
        2/w,    0,  0, -1,
        0, -2/h,  0,  1,
        0,     0,  1, 0,
        0,     0,  0, 1
        );

    desenheQuads(delta, projection);
    desenheDiscos(delta, projection);
    window.requestAnimationFrame(desenhe);
}

que é basicamente a mesma usado no exemplo de animação com apenas discos.

O código completo desse exemplo, disponível abaixo, pode ser visto no JSitor.

12.8. Onde estamos e para onde vamos?

Nessa aula vimos como podemos usar transformações para simplificar o processo de criar desenhos e animações usando WebGL. Aplicando as noções de geometria e álgebra linear que vimos nas últimas aulas vimos que essas transformações podem ser representadas de forma compacta e elegante por meio de matrizes.

Na próxima aula, vamos continuar nossa jornada para aumentar o realismo de nossos desenhos introduzindo conceitos de geometria projetiva para tratar o problema de visualização de objetos em 3D.

12.9. Exercícios

  1. Baseado no Problema 4 da 1a lista de exercícios do Prof. Mount.

No distante planeta de Omicron Persei 8 (OP8), o viewport é definido com origem no canto superior direito. O eixo x aponta para baixo e o eixo y aponta para a esquerda. Seja \(w\) e \(h\) a largura e altura como mostrado na figura abaixo. Considere a região retangular do desenho, em coordenadas normalizadas, com lados esquerdo e direito definidos por \(x_{min}\) e \(x_{max}\) e os lados inferior e superior definidos por \(y_{min}\) e \(y_{max}\).

exercício Viewport do Omicron Persei
    1. Defina a transformação para o viewport que mapeia um ponto \(P=(p_x, p_y)\) na região de desenho normalizada para o ponto correspondente \(V = (v_x, v_y)\) do viewport de OP8. Expresse sua transformação em duas equações

    \(v_x\) = alguma função de \(p_x\) e/ou \(p_y\).

    \(v_y\) = alguma função de \(p_x\) e/ou \(p_y\).

    mostre os passos da sua solução, não apenas a fórmula final.

    1. Suponha que os pontos \(P\) e \(V\) sejam expressos em coordenadas homogêneas \(P = (p_x, p_y, 1)^T\) e \(V = (v_x, v_y, 1)^T\). Expresse a transformação do item anterior com uma matrix \(M\) de dimensão \(3 \times 3\), tal que \(V = M P\).
  1. Baseado no Problema 2 do Homework 2 do Prof. Mount.

Considere os dois sistemas de coordenadas da figura abaixo.

    1. expresse \(P\) e \(\vec{w}\) em coordenadas homogêneas em relação ao sistema \(F\).
    1. expresse \(P\) e \(\vec{w}\) em coordenadas homogêneas em relação ao sistema \(G\).
    1. forneça uma matrix \(M\) de dimensão \(3 \times 3\) que transforma um ponto em coordenadas homogêneas expressas em \(G\) para coordenadas homogêneas em \(F\). Se desejar, pode expressar sua resposta em função de uma matriz inversa, sem necessidade de calcular a inversa.
exercício Mudança de coordenadas.
  1. Baseado no Problema 3 do Homework 2 do Prof. Mount.

Vimos que o WebGL interpola linearmente as cores dos vértices para pintar o interior de um triângulo (conhecido como Gouraud shading). Considere o triângulo da figura abaixo, com vértices em (0,0), (1,0) e (1,1). Seja \(C_0\), \(C_1\) e \(C_2\) as cores RGB correspondentes para cada um desses vértices. Derive uma função \(C(x, y)\) que interpola as cores linearmente e que possa ser usada para pintar um ponto \(Q = (x, y)\) no interior do triângulo. Você pode expressar sua solução como uma fórmula ou na forma de pseudo-código. A cor deve ser uma função de x, y, e as 3 cores \(C_0\), \(C_1\) e \(C_2\). No caso de usar fórmulas, mostre o seu trabalho intermediário para chegar na solução.

exercício combinação afim de 3 cores.

4. Considere o código da animação ilustrada nesse exercício no JSitor. Ao invés de discos, considere agora o seguinte triângulo em coordenadas normalizadas [(0.5, 0), (-0.5,-0.5), (-0.5, 0.5)].

Esse triângulo corresponde a uma forma que pode ser orientada na direção do movimento. Digamos que, em sua orientação inicial, definida pelos vértices, o bico do triângulo (vértice em (0.5, 0)) “aponta” para a direita. Escreva um programa que anime esse triângulo com uma velocidade constante aleatória (vx, vy), e que essa velocidade é refletida quando o objeto bate em uma parede do canvas. O triângulo deve ser rotacionado para que seu “bico” sempre aponte na direção do movimento. Use um fator de escala como (0.3, 0.2) para diminuir o tamanho do triângulo na animação.

12.10. Para saber mais