O WebGL transforma

Gregg Tavares
Gregg Tavares

Translação 2D do WebGL

Antes de passar para o 3D, vamos nos prender ao 2D por um tempo. Sejam comigo, por favor. Este artigo pode parecer excessivamente óbvio para alguns, mas vou chegar ao ponto em alguns artigos.

Este artigo é a continuação de uma série que começa com os Fundamentos do WebGL. Se você ainda não leu, sugiro que leia pelo menos o primeiro capítulo e depois volte aqui. Translação é um nome matemático sofisticado que basicamente significa "mover" alguma coisa. Suponho que mover uma frase do inglês para o japonês também é adequado, mas neste caso estamos falando sobre mover geometria. Usando o exemplo de código que criamos na primeira postagem, você poderia converter facilmente nosso retângulo apenas mudando os valores transmitidos para setRectangle, certo? Confira um exemplo com base em nossa amostra anterior.

  // First lets make some variables 
  // to hold the translation of the rectangle
  var translation = [0, 0];
  // then let's make a function to
  // re-draw everything. We can call this
  // function after we update the translation.
  // Draw the scene.
  function drawScene() {
     // Clear the canvas.
    gl.clear(gl.COLOR_BUFFER_BIT);
    // Setup a rectangle
    setRectangle(gl, translation[0], translation[1], width, height);

    // Draw the rectangle.
    gl.drawArrays(gl.TRIANGLES, 0, 6);
  }

Até agora, tudo bem. Mas agora imagine que queríamos fazer o mesmo com uma forma mais complicada. Digamos que queiramos desenhar um "F" composto por seis triângulos como este.

F letra

A seguir, temos o código atual em que teríamos que mudar o setRectangle para fazer algo mais ou menos assim.

// Fill the buffer with the values that define a letter 'F'.
function setGeometry(gl, x, y) {
  var width = 100;
  var height = 150;
  var thickness = 30;
  gl.bufferData(
      gl.ARRAY_BUFFER,
      new Float32Array([
          // left column
          x, y,
          x + thickness, y,
          x, y + height,
          x, y + height,
          x + thickness, y,
          x + thickness, y + height,

          // top rung
          x + thickness, y,
          x + width, y,
          x + thickness, y + thickness,
          x + thickness, y + thickness,
          x + width, y,
          x + width, y + thickness,

          // middle rung
          x + thickness, y + thickness * 2,
          x + width * 2 / 3, y + thickness * 2,
          x + thickness, y + thickness * 3,
          x + thickness, y + thickness * 3,
          x + width * 2 / 3, y + thickness * 2,
          x + width * 2 / 3, y + thickness * 3]),
      gl.STATIC_DRAW);
}

Espero que você observe que ele não vai se expandir bem. Se quisermos desenhar uma geometria muito complexa com centenas ou milhares de linhas, teríamos que escrever um código bastante complexo. Além disso, toda vez que desenharmos, o JavaScript precisa atualizar todos os pontos. Há uma maneira mais simples. Basta fazer upload da geometria e fazer a translação no sombreador. Este é o novo sombreador

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;

uniform vec2 u_resolution;
uniform vec2 u_translation;

void main() {
   // Add in the translation.
   vec2 position = a_position + u_translation;

   // convert the rectangle from pixels to 0.0 to 1.0
   vec2 zeroToOne = position / u_resolution;
   ...

e vamos reestruturar o código um pouco. Para um, só precisamos definir a geometria uma vez.

// Fill the buffer with the values that define a letter 'F'.
function setGeometry(gl) {
  gl.bufferData(
      gl.ARRAY_BUFFER,
      new Float32Array([
          // left column
          0, 0,
          30, 0,
          0, 150,
          0, 150,
          30, 0,
          30, 150,

          // top rung
          30, 0,
          100, 0,
          30, 30,
          30, 30,
          100, 0,
          100, 30,

          // middle rung
          30, 60,
          67, 60,
          30, 90,
          30, 90,
          67, 60,
          67, 90]),
      gl.STATIC_DRAW);
}

Em seguida, basta atualizar u_translation antes de desenhar com a tradução desejada.

  ...
  var translationLocation = gl.getUniformLocation(
             program, "u_translation");
  ...
  // Set Geometry.
  setGeometry(gl);
  ..
  // Draw scene.
  function drawScene() {
    // Clear the canvas.
    gl.clear(gl.COLOR_BUFFER_BIT);

    // Set the translation.
    gl.uniform2fv(translationLocation, translation);

    // Draw the rectangle.
    gl.drawArrays(gl.TRIANGLES, 0, 18);
  }

Observe que setGeometry é chamado apenas uma vez. Ele não está mais dentro de drawScene.

Agora, quando desenhamos, o WebGL está fazendo praticamente tudo. Tudo o que estamos fazendo é configurar uma translação e pedir que ela desenhe. Mesmo que nossa geometria tivesse dezenas de milhares de pontos, o código principal permaneceria o mesmo.

Rotação 2D do WebGL

Vou admitir desde o início que não tenho ideia se vou explicar isso, mas o que droga, poderia tentar.

Primeiro, quero apresentar o que é chamado de "círculo unitário". Se você se lembra da sua matemática do ensino médio (não vá dormir por minha conta!), um círculo tem um raio. O raio de um círculo é a distância do centro do círculo até a aresta. Um círculo unitário tem um raio de 1,0.

Na matemática básica do 3o ano, se você multiplicar algo por 1, a coisa continua igual. Portanto, 123 * 1 = 123. Bem básico, não é? Um círculo unitário, um círculo com raio de 1,0, também é uma forma de 1. É um 1 giratório. Então você pode multiplicar algo por esse círculo unitário e, de certa forma, é como multiplicar por 1, exceto que a mágica acontece e as coisas giram. Vamos usar os valores de X e Y de qualquer ponto no círculo unitário e multiplicar nossa geometria por eles da amostra anterior. Aqui estão as atualizações do nosso sombreador.

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;

uniform vec2 u_resolution;
uniform vec2 u_translation;
uniform vec2 u_rotation;

void main() {
  // Rotate the position
  vec2 rotatedPosition = vec2(
     a_position.x * u_rotation.y + a_position.y * u_rotation.x,
     a_position.y * u_rotation.y - a_position.x * u_rotation.x);

  // Add in the translation.
  vec2 position = rotatedPosition + u_translation;

E atualizamos o JavaScript para transmitir esses dois valores.

  ...
  var rotationLocation = gl.getUniformLocation(program, "u_rotation");
  ...
  var rotation = [0, 1];
  ..
  // Draw the scene.
  function drawScene() {
    // Clear the canvas.
    gl.clear(gl.COLOR_BUFFER_BIT);

    // Set the translation.
    gl.uniform2fv(translationLocation, translation);

    // Set the rotation.
    gl.uniform2fv(rotationLocation, rotation);

    // Draw the rectangle.
    gl.drawArrays(gl.TRIANGLES, 0, 18);
  }

Por que isso funciona? Vamos analisar os cálculos.

rotatedX = a_position.x * u_rotation.y + a_position.y * u_rotation.x;
rotatedY = a_position.y * u_rotation.y - a_position.x * u_rotation.x;

Vamos supor que você tenha um retângulo e queira girá-lo. Antes de começar a girá-la, o canto superior direito está em 3,0, 9,0. Vamos escolher um ponto no círculo unitário 30 graus no sentido horário a partir da 12:00.

Rotação de 30°

A posição no círculo lá é 0,50 e 0,87

3.0 * 0.87 + 9.0 * 0.50 = 7.1
9.0 * 0.87 - 3.0 * 0.50 = 6.3

É exatamente aí que precisamos que esteja

Desenho de rotação

O mesmo para 60 graus no sentido horário

Rotação de 60°

A posição no círculo lá é 0,87 e 0,50

3.0 * 0.50 + 9.0 * 0.87 = 9.3
9.0 * 0.50 - 3.0 * 0.87 = 1.9

Veja que, à medida que giramos esse ponto no sentido horário para a direita, o valor de X aumenta e o Y diminui. Se continuarmos ultrapassando os 90 graus, X começará a diminuir novamente e Y começará a aumentar. Esse padrão nos dá rotação. Os pontos em um círculo unitário têm outro nome. Eles são chamados de seno e cosseno. Para qualquer ângulo, podemos simplesmente consultar o seno e o cosseno da seguinte forma.

function printSineAndCosineForAnAngle(angleInDegrees) {
  var angleInRadians = angleInDegrees * Math.PI / 180;
  var s = Math.sin(angleInRadians);
  var c = Math.cos(angleInRadians);
  console.log("s = " + s + " c = " + c);
}

Se você copiar e colar o código no Console JavaScript e digitar printSineAndCosignForAngle(30), ele vai mostrar o s = 0.49 c= 0.87. Observação: eu arredonde os números. Se você juntar tudo isso, poderá girar sua geometria para qualquer ângulo que quiser. Basta definir a rotação de acordo com o seno e o cosseno do ângulo para o qual você quer girar.

  ...
  var angleInRadians = angleInDegrees * Math.PI / 180;
  rotation[0] = Math.sin(angleInRadians);
  rotation[1] = Math.cos(angleInRadians);

Espero que tenha feito algum sentido. Agora, um mais simples. Escale.

O que são radianos?

Radianos são uma unidade de medida usada com círculos, rotação e ângulos. Assim como podemos medir distâncias em polegadas, jardas, metros, etc., podemos medir ângulos em graus ou radianos.

Você provavelmente está ciente de que matemática com medidas métricas é mais fácil do que matemática com medidas imperiais. Para converter polegadas em pés, dividimos por 12. Para converter polegadas em jardas, dividimos por 36. Não sei você, mas não consigo dividir por 36 de cabeça. Com a métrica é muito mais fácil. Para converter milímetros em centímetros, divide-se por 10. Para converter milímetros em metros, divide-se por 1.000. Consigo dividir por 1.000 de cabeça.

Radianos versus graus são semelhantes. Graus tornam a matemática difícil. Os radianos facilitam a matemática. Existem 360 graus em um círculo, mas há apenas 2π radianos. Então, uma volta completa é 2π radianos. Meia volta é π radianos. 1/4 volta, ou seja, 90 graus é π/2 radiano. Portanto, se você quiser girar algo em 90 graus, basta usar Math.PI * 0.5. Se quiser girar 45 graus, use Math.PI * 0.25 etc.

Quase toda matemática que envolve ângulos, círculos ou rotação funciona de forma muito simples se você começar a pensar em radianos. Então experimente. Use radianos, não graus, exceto em telas da interface.

Escala 2D do WebGL

O dimensionamento é tão fácil quanto a translação.

Multiplicamos a posição pela escala desejada. Aqui estão as alterações em relação ao nosso exemplo anterior.

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;

uniform vec2 u_resolution;
uniform vec2 u_translation;
uniform vec2 u_rotation;
uniform vec2 u_scale;

void main() {
  // Scale the positon
  vec2 scaledPosition = a_position * u_scale;

  // Rotate the position
  vec2 rotatedPosition = vec2(
     scaledPosition.x * u_rotation.y +
        scaledPosition.y * u_rotation.x,
     scaledPosition.y * u_rotation.y -
        scaledPosition.x * u_rotation.x);

  // Add in the translation.
  vec2 position = rotatedPosition + u_translation;

e, quando desenhamos, adicionamos o JavaScript necessário para definir a escala.

  ...
  var scaleLocation = gl.getUniformLocation(program, "u_scale");
  ...
  var scale = [1, 1];
  ...
  // Draw the scene.
  function drawScene() {
    // Clear the canvas.
    gl.clear(gl.COLOR_BUFFER_BIT);

    // Set the translation.
    gl.uniform2fv(translationLocation, translation);

    // Set the rotation.
    gl.uniform2fv(rotationLocation, rotation);

    // Set the scale.
    gl.uniform2fv(scaleLocation, scale);

    // Draw the rectangle.
    gl.drawArrays(gl.TRIANGLES, 0, 18);
  }

Observe que o dimensionamento por um valor negativo inverte a geometria. Espero que esses três últimos capítulos tenham sido úteis para o entendimento de translação, rotação e escala. A seguir, veremos a magia das matrizes que combinam os três em uma forma muito mais simples e, muitas vezes, mais útil.

Por que um "F"?

A primeira vez que vi alguém usar um "F" foi em uma textura. O “F” em si não é importante. O importante é que você consiga identificar a orientação dele de qualquer direção. Se usássemos um coração ♥ ou um triângulo △, por exemplo, não seria possível saber se ele foi virado horizontalmente. Um círculo ○ seria ainda pior. Um retângulo colorido funcionaria com cores diferentes em cada canto, mas você teria que lembrar qual canto. A orientação de um F é instantaneamente reconhecível.

Orientação F

Qualquer forma que você identifique a orientação funcionaria. Eu só usei o "F" desde que fui apresentado à ideia.

Matrizes 2D do WebGL

Nos últimos três capítulos, vimos como converter, girar e dimensionar geometrias. Translação, rotação e escala são considerados um tipo de "transformação". Cada uma dessas transformações exigiu mudanças no sombreador, e cada uma das três era dependente da ordem.

Por exemplo, aqui está uma escala de 2, 1, rotação de 30% e translação de 100, 0.

Rotação F e translação

E aqui está uma translação de 100,0, rotação de 30% e escala de 2, 1

Rotação F e escala

Os resultados são completamente diferentes. Para piorar, se precisássemos do segundo exemplo, teríamos que escrever um sombreador diferente que aplicasse a translação, rotação e escala na nova ordem desejada. Algumas pessoas mais inteligentes do que eu descobriram que é possível fazer as mesmas coisas com a matemática de matrizes. Para 2d, usamos uma matriz 3x3. Uma matriz 3x3 é como uma grade com 9 caixas.

1.0 2.0 3.0
4.0 5,0 6.0
7,0 8.0 9,0

Para fazer o cálculo, multiplicamos a posição pelas colunas da matriz e somamos os resultados. Nossas posições têm apenas 2 valores, x e y, mas, para fazer esse cálculo, precisamos de 3 valores, portanto, usaremos 1 para o terceiro valor. Neste caso, nosso resultado seria:

newX = x * 1.0 + y * 4.0 + 1 * 7.0

newY = x * 2.0 + y * 5.0 + 1 * 8.0

extra = x * 3.0 + y * 6.0 + 1 * 9.0

Você provavelmente está olhando para isso e pensando "QUAL É O PONTO". Vamos supor que temos uma tradução. Chamaremos a quantidade que queremos traduzir de tx e ty. Vamos fazer uma matriz como esta

1.00,00,0
0,01.00,0
txty1.0

E agora, confira

newX = x * 1.0 + y * 0.0 + 1 * tx

newY = x * 0.0 + y * 1.0 + 1 * ty

extra = x * 0.0 + y * 0.0 + 1 * 1

Se você se lembra da sua álgebra, podemos excluir qualquer lugar que se multiplica por zero. Multiplicar por 1 efetivamente não faz nada, então vamos simplificar para ver o que está acontecendo.

newX = x + tx;
newY = y + ty;

E extra nós realmente não nos importamos. Isso é surpreendentemente parecido com o código de tradução do nosso exemplo de tradução. Da mesma forma, vamos fazer a rotação. Como indicamos no post de rotação, só precisamos do seno e do cosseno do ângulo em que queremos girar.

s = Math.sin(angleToRotateInRadians);
c = Math.cos(angleToRotateInRadians);

E construímos uma matriz como esta

c-s0,0
sc0,0
0,00,01.0

Ao aplicar a matriz, obtemos

newX = x * c + y * s + 1 * 0

newY = x * -s + y * c + 1 * 0

extra = x * 0.0 + y * 0.0 + 1 * 1

Ao ocultar tudo, multiplicamos por 0s e 1s, obtendo-se

newX = x *  c + y * s;
newY = x * -s + y * c;

Que é exatamente o que tínhamos em nossa amostra de rotação. E, por último, a escala. Chamaremos nossos 2 fatores de escala de sx e sy E criaremos uma matriz como esta

sx0,00,0
0,0sy0,0
0,00,01.0

Ao aplicar a matriz, obtemos

newX = x * sx + y * 0 + 1 * 0

newY = x * 0 + y * sy + 1 * 0

extra = x * 0.0 + y * 0.0 + 1 * 1

que é realmente

newX = x * sx;
newY = y * sy;

que é igual à amostra de dimensionamento. Tenho certeza de que você ainda está pensando. E o que isso significa? Qual é o objetivo? Parece que foi muito trabalho só para fazer a mesma coisa que já estávamos fazendo? É aqui que a mágica entra. Pode-se multiplicar matrizes em conjunto e aplicar todas as transformações de uma só vez. Vamos supor que temos a função matrixMultiply, que multiplica duas matrizes e retorna o resultado. Para deixar as coisas mais claras, vamos fazer com que as funções criem matrizes para translação, rotação e escala.

function makeTranslation(tx, ty) {
  return [
    1, 0, 0,
    0, 1, 0,
    tx, ty, 1
  ];
}

function makeRotation(angleInRadians) {
  var c = Math.cos(angleInRadians);
  var s = Math.sin(angleInRadians);
  return [
    c,-s, 0,
    s, c, 0,
    0, 0, 1
  ];
}

function makeScale(sx, sy) {
  return [
    sx, 0, 0,
    0, sy, 0,
    0, 0, 1
  ];
}

Agora, vamos alterar nosso sombreador. O sombreador antigo era assim

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;

uniform vec2 u_resolution;
uniform vec2 u_translation;
uniform vec2 u_rotation;
uniform vec2 u_scale;

void main() {
  // Scale the positon
  vec2 scaledPosition = a_position * u_scale;

  // Rotate the position
  vec2 rotatedPosition = vec2(
     scaledPosition.x * u_rotation.y + scaledPosition.y * u_rotation.x,
     scaledPosition.y * u_rotation.y - scaledPosition.x * u_rotation.x);

  // Add in the translation.
  vec2 position = rotatedPosition + u_translation;
  ...

Nosso novo sombreador será muito mais simples.

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;

uniform vec2 u_resolution;
uniform mat3 u_matrix;

void main() {
  // Multiply the position by the matrix.
  vec2 position = (u_matrix * vec3(a_position, 1)).xy;
  ...

Veja como usamos

  // Draw the scene.
  function drawScene() {
    // Clear the canvas.
    gl.clear(gl.COLOR_BUFFER_BIT);

    // Compute the matrices
    var translationMatrix =
       makeTranslation(translation[0], translation[1]);
    var rotationMatrix = makeRotation(angleInRadians);
    var scaleMatrix = makeScale(scale[0], scale[1]);

    // Multiply the matrices.
    var matrix = matrixMultiply(scaleMatrix, rotationMatrix);
    matrix = matrixMultiply(matrix, translationMatrix);

    // Set the matrix.
    gl.uniformMatrix3fv(matrixLocation, false, matrix);

    // Draw the rectangle.
    gl.drawArrays(gl.TRIANGLES, 0, 18);
  }

Ainda assim, você deve estar se perguntando: "Mas e daí?". Isso não parece um benefício muito grande . Mas, agora, se quisermos mudar a ordem, não precisamos criar um novo sombreador. Podemos apenas mudar os cálculos.

    ...
    // Multiply the matrices.
    var matrix = matrixMultiply(translationMatrix, rotationMatrix);
    matrix = matrixMultiply(matrix, scaleMatrix);
    ...

Ser capaz de aplicar matrizes como essa é especialmente importante para animações hierárquicas, como braços em um corpo, luas em um planeta ao redor de um sol ou galhos em uma árvore. Para um exemplo simples de animação hierárquica, vamos desenhar nosso "F" cinco vezes, mas cada vez vamos começar com a matriz do "F" anterior.

  // Draw the scene.
  function drawScene() {
    // Clear the canvas.
    gl.clear(gl.COLOR_BUFFER_BIT);

    // Compute the matrices
    var translationMatrix = makeTranslation(translation[0], translation[1]);
    var rotationMatrix = makeRotation(angleInRadians);
    var scaleMatrix = makeScale(scale[0], scale[1]);

    // Starting Matrix.
    var matrix = makeIdentity();

    for (var i = 0; i < 5; ++i) {
      // Multiply the matrices.
      matrix = matrixMultiply(matrix, scaleMatrix);
      matrix = matrixMultiply(matrix, rotationMatrix);
      matrix = matrixMultiply(matrix, translationMatrix);

      // Set the matrix.
      gl.uniformMatrix3fv(matrixLocation, false, matrix);

      // Draw the geometry.
      gl.drawArrays(gl.TRIANGLES, 0, 18);
    }
  }

Para fazer isso, introduzimos a função makeIdentity, que cria uma matriz de identidade. Uma matriz de identidade é uma matriz que efetivamente representa 1,0.Assim, se você multiplicar pela identidade, nada acontece. Assim como

X * 1 = X

então também

matrixX * identity = matrixX

Este é o código para criar uma matriz de identidade.

function makeIdentity() {
  return [
    1, 0, 0,
    0, 1, 0,
    0, 0, 1
  ];
}

Mais um exemplo: em cada amostra até agora, nosso "F" gira em torno de seu canto superior esquerdo. Isso ocorre porque o cálculo que estamos usando sempre gira em torno da origem e o canto superior esquerdo de nosso "F" está na origem, (0, 0). Agora, como podemos fazer cálculos matriciais e escolher a ordem de aplicação das transformações, podemos mover a origem antes que o restante das transformações seja aplicado.

    // make a matrix that will move the origin of the 'F' to
    // its center.
    var moveOriginMatrix = makeTranslation(-50, -75);
    ...

    // Multiply the matrices.
    var matrix = matrixMultiply(moveOriginMatrix, scaleMatrix);
    matrix = matrixMultiply(matrix, rotationMatrix);
    matrix = matrixMultiply(matrix, translationMatrix);

Com essa técnica, você pode girar ou dimensionar a partir de qualquer ponto. Agora você sabe como o Photoshop ou o Flash permitem mover o ponto de rotação. Vamos ir ainda mais longe. Se você voltar ao primeiro artigo sobre conceitos básicos do WebGL, vai se lembrar que há um código no sombreador para converter pixels em espaço de corte semelhante a este.

  ...
  // convert the rectangle from pixels to 0.0 to 1.0
  vec2 zeroToOne = position / u_resolution;

  // convert from 0->1 to 0->2
  vec2 zeroToTwo = zeroToOne * 2.0;

  // convert from 0->2 to -1->+1 (clipspace)
  vec2 clipSpace = zeroToTwo - 1.0;

  gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);

Se você analisar cada uma dessas etapas por vez, verá que a primeira, "converter de pixels para 0,0 para 1,0", é uma operação de escala. O segundo também é uma operação de escalonamento. A próxima é uma translação, e a última dimensiona Y por -1. Na verdade, podemos fazer tudo isso na matriz que passamos para o sombreador. Podemos fazer duas matrizes de escala, uma para escalar por 1,0/resolução, outra para dimensionar por 2,0, uma terceira para converter com -1,0, -1,0 e uma quarta para escalar Y por -1. Em seguida, multiplicaremos todas elas. Como o cálculo é simples, vamos apenas criar uma função que crie uma matriz de "projeção" diretamente para uma determinada resolução.

function make2DProjection(width, height) {
  // Note: This matrix flips the Y axis so that 0 is at the top.
  return [
    2 / width, 0, 0,
    0, -2 / height, 0,
    -1, 1, 1
  ];
}

Agora podemos simplificar ainda mais o sombreador. Este é o sombreador de vértice totalmente novo.

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;

uniform mat3 u_matrix;

void main() {
  // Multiply the position by the matrix.
  gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
}
</script>

Em JavaScript, precisamos multiplicar pela matriz de projeção.

  // Draw the scene.
  function drawScene() {
    ...
    // Compute the matrices
    var projectionMatrix =
       make2DProjection(canvas.width, canvas.height);
    ...

    // Multiply the matrices.
    var matrix = matrixMultiply(scaleMatrix, rotationMatrix);
    matrix = matrixMultiply(matrix, translationMatrix);
    matrix = matrixMultiply(matrix, projectionMatrix);
    ...
  }

Também removemos o código que define a resolução. Com essa última etapa, passamos de um sombreador bastante complicado, com 6 a 7 etapas, para um sombreador muito simples, com apenas uma etapa, para a mágica da matemática da matriz.

Espero que este artigo tenha ajudado a desmistificar a matemática matricial. Em seguida, passarei para 3D. Em 3D, o cálculo matricial segue os mesmos princípios e uso. Comecei pelo 2D para tentar facilitar a compreensão.