Cubo 3D usando HTML5 e Canvas: Parte 1

Este é o primeiro artigo de uma série de artigos que pretendem mostrar como criar um cubo 3D animado usando o novo elemento Canvas do HTML5 e JavaScript. O cubo será desenhado usando o contexto 2D do canvas. A série estará dividida em 5 partes:

Nesta parte veremos a teoria de base, e a aplicaremos para criar a simulação do cubo em pontos que pode ver abaixo.

Introdução

O contexto 2D do canvas permite desenhar directamente apenas gráficos 2D. Contudo, aplicando a teoria de computação gráfica a 3 dimensões podemos desenhar gráficos 3D e até mesmo animá-los com rotações.

Nós pretendemos desenhar um cubo, que é um objecto 3D, numa superfície 2D que é o nosso monitor. Infelizmente o desenho não pode ser feito directamente. Para desenhar qualquer objecto 3D é necessário, em primeiro lugar, representar o objecto no espaço 3D. Uma vez no espaço 3D, podemos aplicar operações como por exemplo tanslações e rotações. Finalmente, devemos converter o objecto do espaço 3D para o espaço 2D usando uma operação denominada Projecção.

Resumindo, estes são os passos para desenhar um objecto 3D:
a) Representar o objecto no espaço 3D.
b) Aplicar transformações: translações, rotações, ampliações (estas são as mais comuns).
c) Mapear o objecto para o espaço 2D através de uma operação de projecção.

A seguir exploraremos cada um destes passos.

Representação de um objecto no espaço 3D

Um objecto 3D é constituído por faces que por sua vez são constituídas por vértices. Por exemplo, um cubo é composto por 6 faces e 8 vértices. Portanto, para desenhar um cubo precisaremos de informação sobre os seus vértices bem como sobre as suas faces.

Nesta parte, vamos focar atenção apenas na representação dos pontos. A representão de faces fica para o segundo artigo da série.

Para representar os pontos em 3 dimensões usa-se geralmente um sistema cartesiano 3D. Nesta série usaremos o sistema cartesiano apresentado na figura abaixo. Note que o Z aumenta à medida que “entramos no monitor”. Este esquema é designado LEFT-HANDED. Outra alternativa seria um sistema em que o Z aumenta no sentido contrário (designado RIGHT-HANDED).

Sistema de Coordenadas Cartesiano 3D

Para representar um ponto precisaremos de informação sobre as 3 coordenadas X, Y, e Z. Devemos também ter registado nas nossas mentes a informação sobre a orientação dos eixos do sistema que escolhemos. Neste caso X aumenta para a direita, Y aumenta para cima, e Z aumenta para dentro do monitor.

Vimos como definimos um ponto. Na definição de um objecto 3D é necessário definir todos os seus pontos. No caso de um cubo, é necessário definir as coordenadas dos 8 pontos que o compõem.

Tendo um objecto representado em 3 dimensões podemos aplicar operações como ampliação, translação, e rotação. Estas transformações são aplicadas sobre cada um dos pontos que constitui o objecto.

Transformações

Aos pontos podemos aplicar as seguintes transformações: ampliação, translação e rotação. Acho que os nomes das transformações são suficientemente auto-descritivos. Uma nota deve ser dada em relação às rotações em 3D. Contrariamente ao que acontece no espaço 2D, as rotações no espaço 3D acontecem relativamente a um eixo. Normalmente, X, Y, e Z são usados, mas pode ser um eixo qualquer dentro do espaço 3D.

Abaixo apresento respectivamente as fórmulas para ampliação, translação, e rotação. Nas fórmulas, (x,y,z) são coordenadas do ponto a ser transformado, e (x’,y’,z’) são coordenadas do resultado da transformação.

NOTA: Está fora do escopo desta série apresentar a dedução destas fórmulas. Para os que gostam de Matemática esta matéria está relacionada com Álgebra Linear e Vectorial.

Ampliação

x' = x * k
y' = y * k
z' = z * k

k representa o factor de ampliação. Se k > 1 o objecto é ampliado. Se 0 < k < 1 o objecto é diminuido.

Translação

x' = x + tx
y' = y + ty
z' = z + tz

tx, ty, tz representam as parcelas de translação em cada eixo.

Rotação

Rotação no eixo X
y' = y*cos(a) - z*sin(a)
z' = y*sin(a) + z*cos(a)
x' = x

Rotação no eixo Y:
z' = z*cos(a) - x*sin(a)
x' = z*sin(a) + x*cos(a)
y' = y

Rotação no eixo Z:
x' = x*cos(a) - y*sin(a)
y' = x*sin(a) + y*cos(a)
z' = z

a indica o ângulo de rotação no respectivo eixo.

Agora já sabemos como representar um ponto, e temos as fórmulas para efectuar transformações básicas. O que veremos a seguir é o processo de preparação do ponto 3D para visualização  numa tela 2D.

Projecção do objecto (mapeamento no espaço 2D)

Para visualizar um ponto 3D no espaço 2D aplicamos uma operação denominada projecção. Existem vários tipos de projecção, contudo, a mais usada na computação gráfica é a projecção em perspectiva e é esta que nós usaremos nesta série. Abaixo apresento a fórmula:

x' = x * k / z + metade_cumprimento_canvas
y' = metade_altura_canvas - y * k / z
z' = z

(x’,y’) são o resultado do mapeamento das coordenadas (x,y,z) de 3D para 2D. k é uma constante que influencia o campo de visão. Eu uso normalmente um valor entre 128 e 256.

Já vimos teoria suficiente. Na secção seguinte vamos pôr em prática toda a teoria apresentada.

Código

Primeiro começarei por mostrar como representar um ponto em código JavaScript. No código abaixo apresento a classe Point3D. Esta classe representa pontos em 3 dimensões. Note que incluí nela métodos que implementam as transformações que vimos acima. Todos os métodos retornam um novo ponto (o ponto transformado). Mais tarde veremos que esta abordagem possibilita chaining ou encadeamento de chamadas de métodos.

O método translate() aplica a transformação de translação. A transformação de ampliação é aplicada pelo método scale(). Os métodos rotateX(), rotateY(), e rotateZ(), aplicam a transformação de rotação nos eixos X, Y e Z, respectivamente. Os métodos de rotação esperam ângulos em graus, mas como as funções trigonométricas do JavaScript operam com ângulos em radianos, os angulos são primeiro convertidos para radianos.

function Point3D(x,y,z) {
    this.x = x;
    this.y = y;
    this.z = z;

    this.translate = function(tx,ty,tz) {
        return new Point3D(x + tx,y + ty,z + tz)
    }

    this.rotateX = function(angle) {
        var rad, cosa, sina, y, z
        rad = angle * Math.PI / 180
        cosa = Math.cos(rad)
        sina = Math.sin(rad)
        y = this.y * cosa - this.z * sina
        z = this.y * sina + this.z * cosa
        return new Point3D(this.x, y, z)
    }

    this.rotateY = function(angle) {
        var rad, cosa, sina, x, z
        rad = angle * Math.PI / 180
        cosa = Math.cos(rad)
        sina = Math.sin(rad)
        z = this.z * cosa - this.x * sina
        x = this.z * sina + this.x * cosa
        return new Point3D(x,this.y, z)
    }

    this.rotateZ = function(angle) {
        var rad, cosa, sina, x, y
        rad = angle * Math.PI / 180
        cosa = Math.cos(rad)
        sina = Math.sin(rad)
        x = this.x * cosa - this.y * sina
        y = this.x * sina + this.y * cosa
        return new Point3D(x, y, this.z)
    }

    this.project = function(canvasWidth, canvasHeight, fov) {
        var factor, x, y
        factor = fov / this.z
        x = this.x * factor + canvasWidth / 2
        y = this.y * factor + canvasHeight / 2
        return new Point3D(x, y, this.z)
    }
}

Código Completo

Agora que sabemos como representar um ponto em código, ja podemos construir a simulação que foi apresentada no topo da página. O código abaixo faz isso.

<!DOCTYPE html>
<html>
<head>
    <title>Cubo 3D usando HTML5 e Canvas: Parte 1</title>
    <script type="text/javascript">
        window.onload = startDemo;

        function Point3D(x,y,z) {
            this.x = x;
            this.y = y;
            this.z = z;

            this.scale = function(k) {
                return new Point3D(x * k,y * k,z * k)
            }

            this.translate = function(tx,ty,tz) {
                return new Point3D(x + tx,y + ty,z + tz)
            }

            this.rotateX = function(angle) {
                var rad, cosa, sina, y, z
                rad = angle * Math.PI / 180
                cosa = Math.cos(rad)
                sina = Math.sin(rad)
                y = this.y * cosa - this.z * sina
                z = this.y * sina + this.z * cosa
                return new Point3D(this.x, y, z)
            }

            this.rotateY = function(angle) {
                var rad, cosa, sina, x, z
                rad = angle * Math.PI / 180
                cosa = Math.cos(rad)
                sina = Math.sin(rad)
                z = this.z * cosa - this.x * sina
                x = this.z * sina + this.x * cosa
                return new Point3D(x,this.y, z)
            }

            this.rotateZ = function(angle) {
                var rad, cosa, sina, x, y
                rad = angle * Math.PI / 180
                cosa = Math.cos(rad)
                sina = Math.sin(rad)
                x = this.x * cosa - this.y * sina
                y = this.x * sina + this.y * cosa
                return new Point3D(x, y, this.z)
            }

            this.project = function(viewWidth, viewHeight, fov) {
                var factor, x, y
                factor = fov / this.z
                x = this.x * factor + viewWidth / 2
                y = -this.y * factor + viewHeight / 2
                return new Point3D(x, y, this.z)
            }
        }

        var vertices = [
            new Point3D(-1,1,-1),
            new Point3D(1,1,-1),
            new Point3D(1,-1,-1),
            new Point3D(-1,-1,-1),
            new Point3D(-1,1,1),
            new Point3D(1,1,1),
            new Point3D(1,-1,1),
            new Point3D(-1,-1,1)
        ];

        var angle = 0;

        function startDemo() {
            canvas = document.getElementById("tutorial");
            if( canvas && canvas.getContext ) {
                ctx = canvas.getContext("2d");
                setInterval(loop,33);
            }
        }

        function loop() {
            ctx.fillStyle = "rgb(0,0,0)";
            ctx.fillRect(0,0,400,200);

            for( var i = 0; i < vertices.length; i++ ) {
                var v = vertices[i];
                var r = v.rotateX(angle).rotateY(angle).rotateZ(angle).translate(0,0,5);
                var p = r.project(400,200,200);
                ctx.fillStyle = "rgb(255,255,255)";
                ctx.fillRect(p.x,p.y,2,2);
            }

            angle += 2
        }
    </script>
</head>
<body>
    <canvas id="tutorial" width="400" height="200">
      O seu browser não suporta o elemento canvas do HTML5.
      Por favor, actualize o seu browser.
    </canvas>
</body>
</html>

A classe Point3D representa pontos em 3D. O array vertices armazena os 8 pontos que definem o cubo.

A função startDemo() é invocada quando a página é completamente carregada. A função pega a referência do contexto 2D e configura a função loop() para ser invocada a cada 33 milisegundos. É esta função que corre a animação. Em cada chamada da função, todos os vértices do cubo são transformados com uma rotação sobre X, seguido de uma rotação sobre Y, seguido de uma rotação sobre Z, e termina-se com um translação sobre Z. Note que as transformações são aplicadas usando chaining/encadeamento dos métodos de transformação. Depois das transformações os vértices são projectados e desenhados no canvas. Em cada iteração da função loop() o angulo de rotação é incrementado fazendo o cubo girar continuamente.

Conclusão

Nesta primeira parte da série vimos como representar os pontos do cubo, e como aplicar algumas transformações. Com a teoria aprendida foi possível construir uma animação do cubo representado pelos seus pontos. Na próxima parte, veremos como representar faces e com isso melhoraremos a simulação desenhando o cubo com as suas arestas. Assine o nosso feed para ficar actualizado sobre lançamentos de novos artigos.

Se gostou do artigo, por favor clique num dos botões abaixo para partilhar o artigo. Considere também deixar um comentário expressando o que achou sobre o artigo.

Leave a Comment

Notify me of followup comments via e-mail. You can also subscribe without commenting.