Olá pessoal, tudo certo?!
No post de hoje, exercito um pouco mais “meus limites” com html5. Minha proposta? Reproduzir um “clássico” exemplo de computação gráfica (rotating cube), usando apenas Javascript e html5.
O código-fonte com completo está em https://gist.github.com/1297152 e há um “live demo” em http://users.cjb.net/livedemoelemarjr/wireframe.html
Para não ficar “repetindo”, informo que utilizo o canvas, maximizado na área cliente do browser, usando a técnica que mostrei no post anterior.
Vamos ao que interessa…
Se você quer usar 3D, precisa de algumas matrizes
A menos que você esteja programando com XNA (que tem uma série só para ele aqui no blog), quando começar a trabalhar com 3D precisará ter algumas “matrizes” na manga. Para o projeto de hoje, codifiquei as que precisei criando uma “classe estática” (usando uma técnica que mostrei em outro post). Observe:
var MatrixUtils = { identity: function () { return new Matrix([ [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1] ]); }, rotationZ: function (q) { return new Matrix([ [Math.cos(q), Math.sin(q), 0, 0], [-Math.sin(q), Math.cos(q), 0, 0], [0, 0, 1, 0], [0, 0, 0, 1] ]); }, rotationX: function (q) { return new Matrix([ [1, 0, 0, 0], [0, Math.cos(q), Math.sin(q), 0], [0, -Math.sin(q), Math.cos(q), 0], [0, 0, 0, 1] ]); }, rotationY: function (q) { return new Matrix([ [Math.cos(q), 0, -Math.sin(q), 0], [0, 1, 0, 0], [Math.sin(q), 0, Math.cos(q), 0], [0, 0, 0, 1] ]); }, translation: function (dx, dy, dz) { return new Matrix([ [1, 0, 0, dx], [0, 1, 0, dy], [0, 0, 1, dz], [0, 0, 0, 1] ]); }, scale: function (sx, sy, sz) { return new Matrix([ [sx, 0, 0, 0], [0, sx, 0, 0], [0, 0, sz, 0], [0, 0, 0, 1] ]); } };
Utilizei apenas matrizes básicas aqui. Você conheceu todas elas nas aulas de geometria do ensino médio (se não dormiu nas aulas).
Criando uma classe para representar uma instância de matriz
Como você pode ver em MatrixUtils, utilizo uma classe para representar a matriz. Observe:
function Matrix(cells) { this.cells = cells; this.mult = function (m) { var result = []; for (var i = 0; i < 4; i++) { result[i] = []; for (var j = 0; j < 4; j++) { result[i][j] = this.cells[i][0] * m.cells[0][j] + this.cells[i][1] * m.cells[1][j] + this.cells[i][2] * m.cells[2][j] + this.cells[i][3] * m.cells[3][j]; } } return new Matrix(result) } }
Repare que utilizo essa classe apenas para “armazenar” os valores das células e realizar a operação de multiplicação. Aliás, o poder das matrizes na computação gráfica está no fato destas serem “combináveis” através da multiplicação simples.
Repare como crio uma nova instância de Matrix toda vez que uma multiplicação ocorre. Assim, crio uma pequena interface fluente.
Criando uma classe para representar uma instância de Vetor3D
Se há um conceito matemático, em computação gráfica, mais útil que matrizes, é o conceito de Vetor. Mais uma vez, se você não “dormiu” nas aulas do ensino médio, não terá dificuldade de entender o código que segue:
function Vector3D(x, y, z) { this.x = x; this.y = y; this.z = z; this.perspective = function (viewWidth, viewHeight, fov, viewDistance) { var factor, x, y factor = fov / (viewDistance + this.z) x = this.x * factor + viewWidth / 2 y = this.y * factor + viewHeight / 2 return new Vector3D(x, y, this.z) }; this.mult = function (m) { var nx, ny, nz; nx = m.cells[0][0] * this.x + m.cells[0][1] * this.y + m.cells[0][2] * this.z + m.cells[0][3]; ny = m.cells[1][0] * this.x + m.cells[1][1] * this.y + m.cells[1][2] * this.z + m.cells[1][3]; nz = m.cells[2][0] * this.x + m.cells[2][1] * this.y + m.cells[2][2] * this.z + m.cells[2][3]; return new Vector3D(nx, ny, nz); }; this.normalize = function () { var d = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); return new Vector3D(this.x / d, this.y / d, this.z / d); }; this.cross = function (another) { x = this.y * another.z - this.z * another.y; y = this.z * another.x - this.x * another.z; z = this.x * another.y - this.y * another.x; return new Vector3D(x, y, z); } this.dot = function (another) { return this.X * another.X + this.Y * another.Y + this.Z * another.Z; } }
Embora tenha implantando as funções normalize, cross e dot, não as utilizou hoje. O método perspective é uma solução “feinha” mas suficiente para criar o “efeito” perspectiva na tela.
Criando uma classe para representar um cubo
function Cube(scaleMatrix, rotationMatrix, translationMatrix) { var vertices = [ new Vector3D(-1, 1, -1), new Vector3D(1, 1, -1), new Vector3D(1, -1, -1), new Vector3D(-1, -1, -1), new Vector3D(-1, 1, 1), new Vector3D(1, 1, 1), new Vector3D(1, -1, 1), new Vector3D(-1, -1, 1) ]; var faces = [ [0, 1, 2, 3], [1, 5, 6, 2], [5, 4, 7, 6], [4, 0, 3, 7], [0, 4, 5, 1], [3, 2, 6, 7] ]; this.scaleMatrix = scaleMatrix; this.rotationMatrix = rotationMatrix; this.translationMatrix = translationMatrix; this.draw = function (context, w, h) { var t = new Array(); for (var i = 0; i < 8; i++) { var v = vertices[i]; var r = v .mult(this.scaleMatrix) .mult(this.rotationMatrix) .mult(this.translationMatrix) .perspective( canvasWidth, canvasHeight, Math.min(canvasWidth, canvasHeight) * 0.9, 3.5); t.push(r); } context.strokeStyle = "rgb(255,255,255)" for (var i = 0; i < faces.length; i++) { var f = faces[i]; context.beginPath(); context.moveTo(t[f[0]].x, t[f[0]].y); context.lineTo(t[f[1]].x, t[f[1]].y); context.lineTo(t[f[2]].x, t[f[2]].y); context.lineTo(t[f[3]].x, t[f[3]].y); context.closePath(); context.stroke(); } } }
Já temos vetores e matrizes. Agora, resolvi criar uma entidade desenhável. Perceba:
- utilizo um array de Vector3D para representar os “vértices” do cubo;
- utilizo três objetos Matrix para poder “regualar” a rotação, translação e escala do cubo;
- criei um método draw onde faço a “transformação” dos pontos e desenho linhas no canvas.
Simples?! Espero que sim.
Um conversor simples de Graus para Radianos
As funções que utilizam ângulos do javascript trabalham com radianos. Pessoalmente, prefiro a representação em graus. Por isso, escrevi um pequeno conversor:
function DTR(x) { return x * Math.PI / 180; }
Você também aprendeu essa no ensino médio (quem mandou matar tantas aulas :d)
Implementando o Animation Loop
Outra vez, repito um conceito que apresentei no post anterior. Fique tranquilo, não vou explicar a idéia outra vez.
function loadContent() { entities.push(new Cube( MatrixUtils.scale(0.75, 0.75, 0.75), MatrixUtils.identity(), MatrixUtils.translation(1, 0, 0) )); entities.push(new Cube( MatrixUtils.scale(0.5, 0.5, 0.5), MatrixUtils.identity(), MatrixUtils.translation(-1, 0, 0) )); animate(); } loadContent(); function animate() { update(); draw(); setTimeout(animate, 33); } function update() { entities[0].rotationMatrix = entities[0].rotationMatrix .mult(MatrixUtils.rotationX(DTR(1))) .mult(MatrixUtils.rotationY(DTR(1))) .mult(MatrixUtils.rotationZ(DTR(1))); entities[1].rotationMatrix = entities[1].rotationMatrix .mult(MatrixUtils.rotationX(DTR(-1))); } function draw() { context.clearRect(0, 0, canvasWidth, canvasHeight); for (var i = 0; i < entities.length; i++) { entities[i].draw(context, canvasWidth, canvasHeight); } }
Como você pode ver:
- crio as entidades (todas) no método loadContents.
- atualizo as “matrizes” de cada entidade no método update;
- desenho as entidades no método draw.
Ufa!
Era isso.
Israel
21/10/2011
Bom dia, ótimo post, mas como eu faço para colocar textura ou uma imagem nas faces do cubo?
elemarjr
21/10/2011
Aguarde… Posts no futuro