Matrix3D 101

Atualmente as engines 3D estão muito presentes em aplicações web. Com o ActionScript3 foi possível desenvolver bibliotecas 3D velozes e com isso a tecnologia se difundiu rapidamente pela web. Hoje em dia temos engines 3D muito bem elaboradas como Papervision3D, Away3D, Sandy3D, Alternativa e muitas outras.

O uso é relativamente simples, uma vez que o cálculo por traz de qualquer biblioteca é automatizado e independe do entendimento do desenvolvedor. Porém eu acredito que é extremamente importante entender ao máximo o funcionamento de qualquer aplicação, biblioteca ou classe, seja pelo conhecimento ou para aprimorar algo que ja foi criado, implementando uma nova funcionalidade ou melhorando as existentes.

Independente da maneira como é construída, qualquer engine 3D, independente da liguagem de programação utilizada, compartilham de alguns elementos em comum, um deles é o que chamamos de Matrix de Transformação. Sim matrizes, aquilo que aprendemos no colégio e que não servia para nada até o dia que resolvemos virar desenvolvedores de aplicações gráficas. Mas antes de entender o que é uma matriz de transformação, precisamos entender o que ela faz. Para isso vamos pensar numa engine 3D de uma forma mais simplificada.

Por exemplo, vamos imaginar um cubo como um conjunto de 8 pontos conectados entre si:

cubo

Cada ponto representa uma posição no espaço, essa posição é o que os matemáticos chamam de vetor. Para movimentar o cubos, é preciso movimentar esses pontos seguindo uma determinada ordem, cada ponto se transforma da mesma forma seguindo sua origem. Essa transformação é o que chamamos de matriz de transformação.

Essa transformação pode ocorrer de 3 formas em qualquer um dos eixos:

- Translação
translacao
- Escalonamento
escala
- Rotação
rotacao

Para entender o que é uma Matriz de Transformação, precisamos entender outros conceitos matemáticos como vetores, operações trigonométrigas e operações com matrizes.

Primeiro vamos imaginar um vetor como uma posição no espaço, um conjunto de 3 coordenadas (x, y e z):

vetor

Para modificar essa posição, podemos adicionar um vetor ao outro resultando num terceiro vetor. Vamos estabelecer um vetor inicial de coordenadas x, y e z respectivamente 5, 2, 0 e que a partir dessa posição vamos mover 4, 2 e 3 unidades respectivamente, resultando no vetor final de valor 9, 4 e 3

vetor_soma

Essa transformação que chamamos de translação.

Tomando a translação como a soma de dois vetores, podemos dizer que o escalonamento se da atravéz da multiplicação de um vetor por um número.

vetor_multiplicacao

Esse escalonamento pode acontecer em qualquer um dos eixos, de uma forma simplificada podemos considerar o seguinte:

vetor_x_vetor

*Lembre-se que esse exemplo não está matematicamente correto, mas por hora essa é a melhor forma de entendimento

Para entender como funciona a rotação de um ponto, devemos considerar o seguinte:

Imagine o vetor que representa a posição do ponto (x, y) como o raio (r) de um eixo de rotação que tem um ângulo inicial (a).

Logo, podemos deduzir que:

rotacao_ponto

x = r*cos(a)
y = r*sen(a)

A rotação desse ponto nada mais é do que a soma desse ângulo (a) a outro (b) resultando no ângulo final (a+b) de rotação desse eixo:

x’ = r*cos(a+b)
y’ = r*sen(a+b)

Lembrando duas das identidades trigonométricas, sabemos que:

cos(a+b) = cos(a)*cos(b) – sen(a)*cos(b)
sen(a+b) = sen(a)*cos(b) + sen(b)*cos(a)

Logo:

x’ = r*cos(a+b) = r*cos(a)*cos(b) – r*sen(a)*cos(b)
y’ = r*sen(a+b) = r*sen(a)*cos(b) + r*sen(b)*cos(a)

Substituindo r*cos(a) por x e r*sen(a) por y, deduzimos que:

x’ = x*cos(b) – y*sen(b)
y’ = x*sen(b) + y*cos(b)

Em um sistema de 3 eixos essa rotação se daria em torno do eixo z, logo sua posição nesse eixo não mudaria.

x’ = x*cos(b) – y*sen(b)
y’ = x*sen(b) + y*cos(b)
z’ = z

Transpondo essa rotação para o eixo y temos a seguinte equação

x’ = x*cos(b) + z*sen(b)
y’ = y
z’ = -x*sen(b) + z*cos(b)

e no eixo x

x’ = x
y’ = y*cos(b) – z*sen(b)
z’ = y*sen(b) + z*cos(b)

O importante não é decorar essas fórmulas, mas entender o padrão que elas formam.
Se organizarmos esse valores em uma tabela da seguinte forma:

matriz

Temos uma matriz onde cada linha pertence a uma parte do vetor e cada coluna representa um elemento pelo qual será multiplicado. A primeira linha pode ser lida da seguinte forma:

x’ = x* cos(b) + y* -sin(b) + z* 0

A segunda como

y’ = x* sin(b) + y* cos(b) + z* 0

e a terceira como

z’ = x* 0 + y* 0 + z* 1

A vector can be multiplied by a matrix as this:Podemos considerar essa, como a matriz de rotação em torno do eixo z, onde b é o ângulo de rotação.

Um vetor pode ser multiplicado por uma matriz da seguinte forma:

vetor_matriz1

Aplicando essa mesma lógica para o escalonamento, obtemos uma matriz de escala como essa:

matriz_escala

Matrizes também podem ser multiplicadas (concatenadas) entre si, resultando em uma matriz de mesma ordem:

multiplicacao_matriz

Uma vez que a translação é considerada como vetor e não como matriz, ao invez de utilizar uma matriz de transformação de terceira ordem (3×3) costuma-se utilizar uma matriz de quarta ordem (4×4), onde a ultima linha é composta por zeros e a ultima coluna contém os valores de translação.

matriz_transformacao

Por fim, deve-se concatenar as matrizes de rotação, escala e translação em uma unica matriz de tranformação e aplica-la em um conjunto de pontos. Uma matriz de transformação cujos valores não alteram nada é chamada de matriz identidade, e tem como valores iniciais estes:

matriz_identidade

Bom, vamos ver em um exemplo prático como isso funciona. Para isso vamos criar uma classe básica de matriz de transformação onde vamos implementar métodos de rotação, translação e escala, e depois usa-la para movimentar um cubo 3d.

Apesar de ser um post sobre matrizes 3d, devemos ter em mente que um elemento mais básico é o vetor. Então devemos criar uma classe básica contendo apenas as coordenadas x. y e z a fim de executar a transformação de forma correta.

Lembre-se que essa abordagem é bem básica e tem como propósito desmistificar um pouco o conceito de matriz 3D, lembre-se que existem uma série de operações envolvidas numa engine 3d seja com matrizes ou com vetores além de outros elementos. Outro ponto importante a ser lembrado é que o Flash Player 10 tem suporte para matrizes e vetores 3d, o que implica num desempenho melhor uma vez que os cálculos são feitos internamente.

Vamos ao que interessa.

Primeiro vamos criar nossa classe de vetor chamada Vector3D contendo apenas 3 propriedades:

package basic3d.math {
	public class Vector3D {
		public var x:Number;
		public var y:Number;
		public var z:Number;
 
		public function Vector3D(x:Number = 0, y:Number = 0, z:Number = 0):void{
			this.x = x;
			this.y = y;
			this.z = z;
		}
	}
}

Agora vamos criar uma document class para nosso exemplo e nela criar 8 instâncias da Vector3D representando os pontos do cubo, um conteiner onde o cubo será desenhado e um método que será chamado constantemente para desenhar esses pontos

package {
	import flash.display.Sprite;
 
	import flash.events.Event;
 
	import basic3d.math.Vector3D;
 
	public class Main extends Sprite {
		private var p0:Vector3D;
		private var p1:Vector3D;
		private var p2:Vector3D;
		private var p3:Vector3D;
		private var p4:Vector3D;
		private var p5:Vector3D;
		private var p6:Vector3D;
		private var p7:Vector3D;
 
		private var container:Sprite;
 
		public function Main():void{
			p0 = new Vector3D(-50, -50, -50);
			p1 = new Vector3D(50, -50, -50);
			p2 = new Vector3D(50, 50, -50);
			p3 = new Vector3D(50, 50, -50);
			p4 = new Vector3D(-50, -50, 50);
			p5 = new Vector3D(50, -50, 50);
			p6 = new Vector3D(50, 50, 50);
			p7 = new Vector3D(50, 50, 50);
 
			container = new Sprite();
 
			container.x = 275;
			container.y = 200;
 
			addChild(container);
 
			addEventListener(Event.ENTER_FRAME, render);
		}
		private function render(e:Event):void{
			trace("draw cube");
		}
	}
}

Até agora nada de mais. Vamos então começar a desenhar o cubo com base nos pontos.

points

Olhando para figura acima, podemos deduzir quais pontos estão conectados entre si. Então utilizando suas coordenadas x e y, podemos desenhar o nosso cubo:

private function render(e:Event):void{
	container.graphics.clear();
 
	container.graphics.lineStyle(0, 0xFF0000);
 
	container.graphics.moveTo(p0.x, p0.y);
	container.graphics.lineTo(p1.x, p1.y);
	container.graphics.lineTo(p2.x, p2.y);
	container.graphics.lineTo(p3.x, p3.y);
	container.graphics.lineTo(p0.x, p0.y);
 
	container.graphics.moveTo(p4.x, p4.y);
	container.graphics.lineTo(p5.x, p5.y);
	container.graphics.lineTo(p6.x, p6.y);
	container.graphics.lineTo(p7.x, p7.y);
	container.graphics.lineTo(p4.x, p4.y);
 
	container.graphics.moveTo(p0.x, p0.y);
	container.graphics.lineTo(p4.x, p4.y);
 
	container.graphics.moveTo(p1.x, p1.y);
	container.graphics.lineTo(p5.x, p5.y);
 
	container.graphics.moveTo(p2.x, p2.y);
	container.graphics.lineTo(p6.x, p6.y);
 
	container.graphics.moveTo(p3.x, p3.y);
	container.graphics.lineTo(p7.x, p7.y);
}

cubo0

Parece mais um quadrado do que um cubo, certo? Isso acontece porque os pontos p0, p1, p2 e p3 tem a mesma posição x e y dos pontos p4, p5, p6 e p7 respectivamente. Dessa forma eles se sobrepõe no espaço bidimensional (x e y). Num espaço tridimencional temos o eixo z para diferenciar as coisas, além disso precisamos considerar a perspectiva dos objectos, um ponto de fuga onde todos os pontos convergem.

Para isso devemos mudar criar um método que faça essa conversão do 3D para o 3D, vamos chama-lo de getPerspective. Esse método vai receber um vetor e retornar um ponto:

private function getPerspective(vector:Vector3D):Point {
	var focalLength:Number = 1000;
	var scaleFactor:Number = focalLength/(focalLength + vector.z);
 
	var point:Point = new Point()
	point.x = vector.x*scaleFactor;
	point.y = vector.y*scaleFactor;
 
	return point;
}

cubo1

Agora ja conseguimos ver algo mais próximo de um cubo. Mas antes de entender o que está acontecendo, vamos olhar mais de perto o método criado. Repare nessas duas linhas:

var focalLength:Number = 1000;
var scaleFactor:Number = focalLength/(focalLength + vector.z);

o parâmetro focalLength representa o foco da nossa “camera”. Para entender isso vamos pensar o seguinte. Quanto mais afastado o objeto está, menor ele fica e vice versa. A distância focal representa a distancia entre a câmera e o objeto tal que sua escala seja 1, ou seja o objeto tem seu tamanho real. Quanto menor a distancia focal, maior é a variação de escala no eixo z. Para testar isso vamos mudar nosso foco para 100:

cubo2

Comparando as duas imagens, vemos que o quadrado exterior ficou muito maior, enquanto o quadrado interior ficou muito menor, isso por que os pontos que formam esses quadrados tiveram uma variação de escala muito maior no eixo z. Mas por hora vamos manter o foco em 1000.

Agora precisamos criar nossa matriz de transformação para poder transformar esses pontos da maneira como quiser. Primeiro vamos definir as propriedades dessa matriz:

package basic3d.math {
	public class Matrix3D {
 
		/**
		 * X O O O
		 * O O O O
		 * O O O O
		*/
		public var a :Number;
 
		/**
		 * O X O O
		 * O O O O
		 * O O O O
		*/
		public var b :Number;
 
		/**
		 * O O X O
		 * O O O O
		 * O O O O
		*/
		public var c :Number;
 
		/**
		 * O O O O
		 * X O O O
		 * O O O O
		*/
		public var d :Number;
 
		/**
		 * O O O O
		 * O X O O
		 * O O O O
		*/
		public var e :Number;
 
		/**
		 * O O O O
		 * O O X O
		 * O O O O
		*/
		public var f :Number;
 
		/**
		 * O O O O
		 * O O O O
		 * X O O O
		*/
		public var g :Number;
 
		/**
		 * O O O O
		 * O O O O
		 * O X O O
		*/
		public var h :Number;
 
		/**
		 * O O O O
		 * O O O O
		 * O O X O
		*/
		public var i :Number;
 
		/**
		 * O O O X
		 * O O O O
		 * O O O O
		*/
		public var tx :Number;
 
		/**
		 * O O O O
		 * O O O X
		 * O O O O
		*/
		public var ty :Number;
 
		/**
		 * O O O O
		 * O O O O
		 * O O O X
		*/
		public var tz :Number;
 
		public function Matrix3D(a:Number = 1, b:Number = 0, c:Number = 0, d:Number = 0, e:Number = 1, f:Number = 0, g:Number = 0, h:Number = 0, i:Number = 1, tx:Number = 0, ty:Number = 0, tz:Number = 0):void{
			this.a = a;
			this.b = b;
			this.c = c;
 
			this.d = d;
			this.e = e;
			this.f = f;
 
			this.g = g;
			this.h = h;
			this.i = i;
 
			this.tx = tx;
			this.ty = ty;
			this.tz = tz;
		}
	}
}

Repare que, por padrão, a matriz assume os valores da matriz identidade.

Agora vamos criar os métodos de transformação dessa matriz. Primeiro o método de concatenação (multiplicação) de matrizes, a partir desse método que vamos efetuar qualquer transformação em nossa matriz.

public function concat(m:Matrix3D):void{
	var values:Object = {};
 
	values.a = a*m.a+d*m.b+g*m.c;	values.b = b*m.a+e*m.b+h*m.c;	values.c = c*m.a+f*m.b+i*m.c;	values.tx = a*m.tx+d*m.ty+g*m.tz+tx;
	values.d = a*m.d+d*m.e+g*m.f;	values.e = b*m.d+e*m.e+h*m.f;	values.f = c*m.d+f*m.e+i*m.f;	values.ty = b*m.tx+e*m.ty+h*m.tz+ty;
	values.g = a*m.g+d*m.h+g*m.i;	values.h = b*m.g+e*m.h+h*m.i;	values.i = c*m.g+f*m.h+i*m.i;	values.tz = c*m.tx+f*m.ty+i*m.tz+tz;
 
	for(var v:String in values){
		this[v] = values[v];
	}
}

Um pouco confuso, não? Se você não entendeu muito bem como funciona a multiplicação de matrizes você pode conferir esse link

Agora vamos começar a rotacionar nosso cubo. Vamos testar apenas a rotação no eixo y e depois replicar o conceito para os outros eixos.

Primeiro vamos lembrar da fórmula usada para rotação em torno do eixy y:

x’ = x*cos(b) + z*sin(b)
y’ = y
z’ = -x*sin(b) + z*cos(b)

A partir dessa fórmula podemos criar uma matriz assim:

matriz_rotacao

Lembrando que a transformação nada mais é do que a concatenação entre a matriz atual com a matriz de transformação, nesse caso devemos multiplica a matriz atual pela matriz de rotação no eixo y.

public function rotateY(angle:Number):void {
	var matrix = new Matrix3D();
 
	matrix.a = Math.cos(angle); 	matrix.b = 0;	matrix.c = Math.sin(angle);
	matrix.d = 0;					matrix.e = 1;	matrix.f = 0;
	matrix.g = -Math.sin(angle);	matrix.h = 0;	matrix.i = Math.cos(angle);
 
	concat(matrix);
}

Para testar primeiro precisamos criar uma variável contendo o valor de rotação em radianos:

private var rotationY:Number = Math.PI*.25;

Nesse caso a rotação está em 45º

Depois precisamos criar uma matriz base e aplicar essa rotação

var matrix:Matrix3D = new Matrix3D();
matrix.rotateY(rotationY);

Depois devemos aplicar essa transformação para cada vetor. Para isso vamos criar um método chamado transform, que recebe e retorna um vetor. Esse método executa a multiplicação entre um vetor e uma matriz e adiciona os valores de translação respectivamente.

public function transform(v:Vector3D):Vector3D {
	var vector:Vector3D = new Vector3D();
 
	vector.x = a*v.x+d*v.y+g*v.z+tx;
	vector.y = b*v.x+e*v.y+h*v.z+ty;
	vector.z = c*v.x+f*v.y+i*v.z+tz;
 
	return vector;
}

Aplicando a transformação nos vetores, temos a seguinte modificação no metodo render:

var point0:Point = getPerspective(matrix.transform(p0));
var point1:Point = getPerspective(matrix.transform(p1));
var point2:Point = getPerspective(matrix.transform(p2));
var point3:Point = getPerspective(matrix.transform(p3));
var point4:Point = getPerspective(matrix.transform(p4));
var point5:Point = getPerspective(matrix.transform(p5));
var point6:Point = getPerspective(matrix.transform(p6));
var point7:Point = getPerspective(matrix.transform(p7));

Alé disso vamos aumentar 1º no angulo de rotação a cada frame:

rotationY += Math.PI/180;

Com isso, ja conseguimos visualizar um cubo rotacionando em torno do eixo y

This movie requires Flash Player 9

Agora vamos replicar a rotação em y para os eixos x e z

public function rotateX(angle:Number):void {
	var matrix = new Matrix3D();
 
	matrix.a = 1; 		matrix.b = 0;			matrix.c = 0;
	matrix.d = 0;		matrix.e = Math.cos(angle);	matrix.f = -Math.sin(angle);
	matrix.g = 0;		matrix.h = Math.sin(angle);	matrix.i = Math.cos(angle);
 
	concat(matrix);
}
public function rotateZ(angle:Number):void {
	var matrix = new Matrix3D();
 
	matrix.a = Math.cos(angle); 	matrix.b = -Math.sin(angle);	matrix.c = 0;
	matrix.d = Math.sin(angle);	matrix.e = Math.cos(angle);	matrix.f = 0;
	matrix.g = 0;			matrix.h = 0;			matrix.i = 1;
 
	concat(matrix);
}

Vamos criar os paramteros de rotação para esses eixos:

private var rotationX:Number = 0;
private var rotationZ:Number = 0;

Acrescentar 1º a cada frame:

rotationX += Math.PI/180;
rotationZ += Math.PI/180;

E chamar os métodos de rotação:

matrix.rotateX(rotationX);
matrix.rotateZ(rotationZ);

O resultado é esse:

This movie requires Flash Player 9

Agora sim conseguimos ver um cubo 3D rotacionando. Mas nosso trabalho não termina por aqui, ainda precisamos mudar a escala e a translação do cubo.
Vamos criar a matriz de transformação da escala:

<span lang="pt">p</span>ublic function scale(sx:Number, sy:Number, sz:Number):void {
	var matrix = new Matrix3D();
 
	matrix.a = sx; 	matrix.b = 0;	matrix.c = 0;
	matrix.d = 0;	matrix.e = sy;	matrix.f = 0;
	matrix.g = 0;	matrix.h = 0;	matrix.i = sz;
 
	concat(matrix);
}

Diferente da rotação onde para cada eixo é preciso uma matriz diferente, a matriz de escala comporta os valores de transformação nos 3 eixos.

Vamos testar primeiro a escala uniforme do nosso cubo. Por hora vamos bloquear a rotação nos eixos x e z e implementar o escalonamento. Nesse exemplo vou utilizar uma senoide para aumentar e diminuir o cubo conforme o tempo:

private var sinCurve:Number = 0;
 
sinCurve += Math.PI/180;
var scale:Number = 1+Math.sin(sinCurve)*.5;
matrix.scale(scale, scale, scale);

Podemos ver que o cubo aumenta e diminui em 50% do seu tamanho.

This movie requires Flash Player 9

Agora vamos criar o método responsável pela translação da matriz.

public function translate(x:Number, y:Number, z:Number):void {
	tx = x;
	ty = y;
	tz = z;
}

Como a translação é considerada como uma coluna extra na matriz, a unica coisa que devemos fazer é modificar seus valores. Para testar vamos utilizar a mesma senoide, mas dessa vez aplica-la na translação no eixo x:

sinCurve += Math.PI/180;
 
var xPos:Number = Math.sin(sinCurve)*150;
matrix.translate(xPos, 0, 0);

Vemos que a posição x do cubo varia entre 150 e -150 pixels conforme o tempo.

This movie requires Flash Player 9

Espero ter ajudado a entender um pouco sobre matrizes de transformação. Lembre-se que existem muito mais operações matematicas envolvendo matrizes e vetores numa engine 3d, além de outras coisas como UV map, z-Sorting e muitas outras coisas.

Um livro que eu recomendo para quem gostaria de se aprofundar nisso é Essential Mathematics for Games and Interactive Applications do James M. Van Verth e Lars M. Bishop. Ele tem uma abordagem muito boa dos conceitos matemáticos aplicados na computação gráfica. Apesar de ser direcionado para programadores C/C++, a matemática é explicada sem seguir nenhuma linguagem.

Source

Qualquer dúvida, sugestão ou crítica, por favor entrem em contato. Ficarei feliz em ajudar no que eu puder.

Obrigado
Referências:
http://wally.cs.iupui.edu/
http://www.prof2000.pt/
http://pt.wikibooks.org/
http://educacao.uol.com.br/matematica/
http://rrgoncalez.wordpress.com/
http://pt.wikipedia.org/wiki/Produto_de_matrizes
http://pt.wikibooks.org/wiki/Matem%C3%A1tica_elementar/Matrizes

Tags: , ,

4 Responses to “Matrix3D 101”

  1. Paulo Afonso says:

    Muito bom post.
    Obrigado

  2. Onur says:

    Thank you for this great tutorial.
    its realy what i need.
    i have a question, if i want skew or twist the cube. How a method and algorithm should i use?
    Thank you again.

  3. admin says:

    I never tried, but i think it’s possible. You can define a formula to each value of the matrix and create a behaviour like skew, twist, bend and any other modifier.

    But this could be another class that extends the Matrix3D.

  4. admin says:

    valew!!!
    Existem outras coisas que podem ser feitas, se tiver alguma duvida ficarei feliz em responder.

Leave a Reply