O designer de layout não precisa de matemática 2: matrizes, transformações básicas, construção 3D e filtros para imagens





A última vez que falamos sobre os gráficos e trajetórias para a animação em stop motion, hoje será sobre a matriz. Vamos descobrir como construir transformações básicas em CSS, SVG e WebGL, construir uma exibição do mundo 3D na tela com nossas próprias mãos, ao longo do caminho desenhando um paralelo com uma ferramenta como Three.js, e também experimentar com filtros para fotos e descobrir o que pois tal magia está em seu âmago.



Gostaria de lembrar que nesta série de artigos nos familiarizamos com várias coisas do campo da matemática que assustam os designers de layout, mas podem ser úteis na resolução de problemas de trabalho. Procuramos evitar teorizações desnecessárias, preferindo imagens e explicações nos dedos, com ênfase em aplicações práticas no frontend. A este respeito, as formulações em alguns lugares podem não ser inteiramente precisas do ponto de vista da matemática, ou não totalmente completas. O objetivo deste artigo é dar uma ideia geral do que acontece e por onde começar se algo acontecer.



Os scripts para gerar imagens no estilo desta série de artigos estão no GitHub , portanto, se você quiser fazer o mesmo, sabe o que fazer.



Poucas definições



Uma matriz em matemática é tal abstração, podemos dizer que é um tipo de dados em certo sentido, e escrevê-lo na forma de uma tabela retangular. O número de colunas e linhas pode ser qualquer coisa, mas na web quase sempre estamos lidando com matrizes quadradas 2x2, 3x3, 4x4 e 5x5.



Também precisamos de uma definição como vetor. Acho que da geometria escolar você pode se lembrar da definição associada às palavras "comprimento" e "direção", mas em geral, na matemática, muitas coisas podem ser chamadas de vetor. Em particular, falaremos sobre um vetor como um conjunto ordenado de valores. Por exemplo, coordenadas da forma (x, y) ou (x, y, z), ou uma cor no formato (r, g, b) ou (h, s, l, a), etc. Dependendo de quantos elementos estão incluídos em tal conjunto, falaremos sobre um vetor de uma dimensão ou outra: se dois elementos são bidimensionais, três são tridimensionais, etc. Além disso, no âmbito dos tópicos em consideração, às vezes pode ser conveniente pensar em um vetor como uma matriz de tamanhos 1x2, 1x3, 1x4, etc. Tecnicamente, poderíamos nos limitar apenas ao termo "matriz", mas ainda usaremos a palavra "vetor" para separar esses dois conceitos um do outro,pelo menos em um sentido lógico.



Para matrizes, assim como para vetores, são definidas várias operações que podem ser feitas com eles. Em particular, multiplicação. Estamos constantemente os multiplicando entre nós. O algoritmo de multiplicação em si não é muito complicado, embora possa parecer um pouco confuso:



function multiplyMatrices(a, b) {
    const m = new Array(a.length);

    for (let row = 0; row < a.length; row++) {
        m[row] = new Array(b[0].length);

        for (let column = 0; column < b[0].length; column++) {
            m[row][column] = 0;

            for (let i = 0; i < a[0].length; i++) {
                m[row][column] += a[row][i] * b[i][column];
            }
        }
    }

    return m;
}


Mas para nós, de fato, não é tão importante lembrar constantemente o princípio de seu funcionamento na solução de problemas do dia a dia. Aqui, nós o mencionamos para ser completo, para fornecer contexto para exemplos adicionais.



Ao lidar com entidades complexas em matemática, é muito útil abstrair. Como aqui - frequentemente falaremos sobre multiplicação, mas não prestaremos atenção a que tipo de operações aritméticas em que ordem ocorrem lá. Sabemos que a multiplicação está definida - e isso é o suficiente para o trabalho.


Só usaremos matrizes quadradas em um conjunto de problemas muito específico, portanto, um conjunto de regras simples será suficiente:



  • Você só pode multiplicar matrizes da mesma dimensão.
  • Multiplicamos a matriz pela matriz - obtemos a matriz.
  • Você pode multiplicar uma matriz por um vetor - obtemos um vetor.
  • A ordem de multiplicação é importante.






Usaremos principalmente a multiplicação da esquerda para a direita, pois é mais familiar e adequada para explicações, mas em alguns livros ou bibliotecas você pode encontrar uma notação da direita para a esquerda, e todas as matrizes serão espelhadas diagonalmente. Isso não afeta de forma alguma a essência das manipulações que estão ocorrendo, então não vamos insistir nisso, mas se você copiar e colar algo, preste atenção.


Também para trabalhos futuros, precisaremos de um conceito como a matriz de identidade. Esta é uma matriz com uns na diagonal principal e zeros em todas as outras células. O algoritmo de multiplicação é construído de forma que multiplicando a matriz de identidade por outra matriz - obtemos a mesma matriz. Ou um vetor, se estamos falando de um vetor. Em outras palavras, a matriz de identidade atua como uma unidade na multiplicação usual de números. Isso é uma coisa neutra que "não afeta nada" quando multiplicada.







E a última coisa que precisamos é o exemplo mostrado na imagem acima. É como um caso especial de multiplicação de uma matriz por um vetor, quando a última linha da matriz é "um pedaço da matriz identidade" e o último elemento do vetor também é igual a 1.



Neste exemplo, estamos usando as letras (x, y) e, como você deve ter adivinhado, a discussão a seguir se concentrará nas coordenadas em 2D. Mas por que adicionar uma terceira coordenada e deixá-la como uma? - você pergunta. É tudo uma questão de conveniência ou, melhor ainda, versatilidade. Muitas vezes adicionamos a coordenada +1 para simplificar os cálculos e trabalhar com 2D vai com matrizes 3x3, trabalhamos com 3D - com matrizes 4x4 e trabalhamos com 4D, por exemplo, com cores no formato (r, g, b, a) vai com matrizes 5x5. À primeira vista, parece uma ideia maluca, mas depois veremos como ela unifica todas as operações. Se você quiser entender este tópico em mais detalhes, você pode pesquisar no Google a expressão "coordenadas uniformes".



Mas chega de teoria, vamos passar à prática.



I. Transformações básicas em computação gráfica



Vamos pegar as expressões do exemplo acima e vê-las como são, fora do contexto de matrizes:



newX = a*x + b*y + c
newY = d*x + e*y + f


Você pode pensar nisso como as equações paramétricas que traçamos da última vez. O que acontece se você definir esses ou aqueles coeficientes neles? Vamos começar com a próxima opção:



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


Nada muda aqui - as novas coordenadas (x, y) são idênticas às antigas. Se substituirmos esses coeficientes na matriz e olharmos de perto, veremos que obtemos a matriz de identidade.



O que acontece se tomarmos outros coeficientes? Por exemplo, são eles:



newX = 1*x + 0*y + A = x + A
newY = 0*x + 1*y + 0 = y


Obteremos um deslocamento ao longo do eixo X. Mas o que mais poderia ter acontecido aqui? Se isso não for óbvio para você, então é melhor retornar à primeira parte, onde falamos sobre gráficos e coeficientes.



Mudando esses 6 coeficientes - a, b, c, d, e, f - e observando as mudanças em xey, mais cedo ou mais tarde chegaremos a quatro de suas combinações, que parecem úteis e convenientes para o uso prático. Vamos escrevê-los imediatamente na forma de matrizes, voltando ao exemplo original:







Os nomes dessas matrizes falam por si. Ao multiplicar essas matrizes por vetores com as coordenadas de alguns pontos, objetos na cena, etc. obtemos novas coordenadas para eles. Além disso, operamos com transformações intuitivas - movimento, escala, rotação e inclinação, e os coeficientes determinam a gravidade de uma determinada transformação ao longo dos eixos correspondentes.



Freqüentemente, é conveniente pensar em matrizes como transformações de algo, como coordenadas. Esta é outra palavra sobre abstrações.


As transformações podem ser empilhadas. Em termos de matrizes, usaremos a operação de multiplicação, que pode ser um pouco confusa, mas é uma sobrecarga da linguagem falada. Se precisarmos deslocar algum objeto para o lado e aumentar, então podemos pegar uma matriz para deslocamento, uma matriz para escalar e multiplicá-los. O resultado será uma matriz que fornece deslocamento e escala ao mesmo tempo. Resta transformar cada ponto de nosso objeto com sua ajuda.







Transformações básicas em CSS



Mas tudo isso são palavras. Vamos ver como fica em um front-end real. Em CSS, nós (de repente) temos uma função de matriz. É algo assim no contexto do código:



.example {
    transform: matrix(1, 0, 0, 1, 0, 0);
}


Muitos novatos que o veem pela primeira vez são abordados pela pergunta - por que existem seis parâmetros? Isto é estranho. Teria sido 4 ou 16 - ainda não foi para onde, mas 6? O que eles estão fazendo?



Mas, na realidade, tudo é simples. Esses seis parâmetros são os próprios coeficientes a partir dos quais acabamos de montar as matrizes para as transformações básicas. Mas, por alguma razão, eles foram organizados em uma ordem diferente:







também no CSS há uma função matrix3d ​​para definir uma transformação 3D usando uma matriz. Já existem 16 parâmetros, exatamente para fazer uma matriz 4x4 (não se esqueça que adicionamos +1 dimensão).



Matrizes para transformações 3D básicas são construídas da mesma maneira que para 2D, apenas mais coeficientes são necessários para organizá-los não em duas coordenadas, mas em três. Mas os princípios são os mesmos.


Naturalmente, toda vez seria estranho cercar a matriz e monitorar a colocação correta dos coeficientes ao trabalhar com transformações simples em CSS. Nós, programadores, geralmente tentamos tornar nossa vida mais fácil. Portanto, agora temos funções curtas em CSS para criar transformações individuais - translateX, translateY, scaleX, etc. Normalmente os usamos, mas é importante entender que por dentro eles criam as mesmas matrizes das quais falamos, simplesmente escondendo esse processo de nós atrás de outra camada de abstração.



As mesmas transformações de translação, rotação, escala e inclinação, bem como a função de matriz universal para definir transformações, estão presentes em SVG. A sintaxe é um pouco diferente, mas a essência é a mesma. Ao trabalhar com gráficos 3D, por exemplo com WebGL, também recorreremos às mesmas transformações. Mas mais sobre isso depois, agora é importante entender que eles estão em todos os lugares e funcionam em todos os lugares de acordo com o mesmo princípio.



Subtotais



Vamos resumir o acima:



  • As matrizes podem ser usadas como transformações para vetores, em particular para as coordenadas de alguns objetos na página.
  • Quase sempre operamos com matrizes quadradas e adicionamos +1 dimensão para simplificar e unificar os cálculos.
  • Existem 4 transformações básicas - translação, rotação, escala e inclinação. Eles são usados ​​em todos os lugares, desde CSS a WebGL e funcionam de maneira semelhante em todos os lugares.


II. Construção de cena 3D DIY



Um desenvolvimento lógico do tópico sobre transformação de coordenadas será a construção de uma cena 3D e sua exibição na tela. De uma forma ou de outra, essa tarefa geralmente está presente em todos os cursos de computação gráfica, mas em cursos front-end geralmente não está. Veremos, talvez um pouco simplificado, mas uma versão completa de como você pode fazer uma câmera com diferentes ângulos de visão, quais operações são necessárias para calcular as coordenadas de todos os objetos na tela e construir uma imagem, e também traçar paralelos com Three.js - o mais popular ferramenta para trabalhar com WebGL.



Uma questão razoável deve surgir aqui - por quê? Por que aprender a fazer tudo com as mãos se você tem uma ferramenta pronta? A resposta está em problemas de desempenho. Você provavelmente já visitou sites com concursos como Awwwards, CSS Design Awards, FWA e similares. Lembra-se de como os sites de desempenho estão participando desses concursos? Sim, quase todo mundo desacelera, atrasa ao carregar e faz o laptop zumbir como um avião! Sim, claro, o principal motivo geralmente são shaders complexos ou muita manipulação de DOM, mas o segundo é a incrível quantidade de scripts. Isso tem um efeito desastroso no carregamento de tais sites. Normalmente tudo acontece assim: você precisa fazer algo no WebGL - pegue algum tipo de mecanismo 3D (+ 500 KB) e alguns plug-ins para ele (+ 500 KB);você precisa fazer um objeto cair ou algo voando - eles levam um motor de física (+ 1 MB, ou até mais); você precisa atualizar alguns dados na página - bem, adicione algum framework SPA com uma dúzia de plug-ins (+ 500 KB), etc. E desta forma, vários megabytes de scripts são digitados, que não só precisam ser baixados pelo cliente (e isso além das imagens grandes), mas também o navegador fará algo com eles após o download - eles não voam apenas para ele. Além disso, em 99% dos casos, até que os scripts funcionem, o usuário não verá toda a beleza que precisaria mostrar desde o início.o que o cliente precisa baixar (e isso além das imagens grandes), então o navegador fará algo com elas após o carregamento - elas não vêm até ele por um motivo. Além disso, em 99% dos casos, até que os scripts funcionem, o usuário não verá toda a beleza que precisaria mostrar desde o início.o que o cliente precisa baixar (além das imagens grandes), então o navegador fará algo com eles após o carregamento - eles não vêm até ele por um motivo. Além disso, em 99% dos casos, até que os scripts funcionem, o usuário não verá toda a beleza que precisaria mostrar desde o início.



Uma crença popular diz que cada 666 KB de scripts em produção aumenta o tempo de carregamento da página por tempo suficiente para um usuário enviar um desenvolvedor de site para o próximo círculo do inferno. Three.js na configuração mínima pesa 628 KB ...


Além disso, muitas vezes as tarefas simplesmente não requerem a conexão de ferramentas complexas. Por exemplo, para mostrar alguns planos com texturas em WebGL e adicionar alguns sombreadores para fazer as imagens divergirem em ondas, você não precisa de Three.js completo. E para fazer um objeto cair, você não precisa de um motor de física completo. Sim, provavelmente irá acelerar seu trabalho, especialmente se você estiver familiarizado com ele, mas você pagará por isso com o tempo dos usuários. Aqui cada um decide por si o que é mais lucrativo para si.



Cadeia de transformação de coordenadas



Na verdade, a essência das transformações de coordenadas para a construção de uma cena 3D em sua tela é bastante simples, mas ainda iremos analisá-la passo a passo, uma vez que muito provavelmente para muitos designers de layout, esse processo será algo novo.



Então é isso. Digamos que um designer tenha desenhado um modelo 3D. Que seja um cubo (nos exemplos usaremos as construções mais simples para não complicar a ilustração de repente):







Como é esse modelo? Na verdade, é um conjunto de pontos em algum sistema de coordenadas e um conjunto de relações entre eles para que você possa determinar entre quais pontos os planos devem ser localizados. A questão dos aviões no contexto do WebGL recairá sobre os ombros do próprio navegador, e as coordenadas são importantes para nós. Você precisa descobrir exatamente como transformá-los.



O modelo, como dissemos, possui um sistema de coordenadas. Mas normalmente queremos ter muitos modelos, queremos fazer uma cena com eles. A cena, nosso mundo 3D, terá seu próprio sistema de coordenadas global. Se apenas interpretarmos as coordenadas do modelo como coordenadas globais, nosso modelo estará localizado como se estivesse "no centro do mundo". Em outras palavras, nada mudará. Mas queremos adicionar muitos modelos a diferentes lugares do nosso mundo, algo como este:







O que fazer? Você precisa converter as coordenadas de cada modelo individual em coordenadas globais. A posição do modelo no espaço é definida por deslocamentos, rotações e escala - já vimos essas transformações básicas. Agora precisamos construir uma matriz de transformação para cada modelo, que armazenará em si apenas essas informações sobre onde o modelo está em relação ao mundo e como ele é girado.



Por exemplo, para cubos, haverá aproximadamente as seguintes matrizes:



//     .
//   «  »      .
const modelMatrix1 = [
    [1, 0, 0, 0],
    [0, 1, 0, 0],
    [0, 0, 1, 0],
    [0, 0, 0, 1]
];

// ,     X.
const modelMatrix2 = [
    [1, 0, 0, 1.5],
    [0, 1, 0, 0  ],
    [0, 0, 1, 0  ],
    [0, 0, 0, 1  ]
];

// ,     X   .
const modelMatrix3 = [
    [1, 0, 0, -1.5],
    [0, 1, 0, 0   ],
    [0, 0, 1, 0   ],
    [0, 0, 0, 1   ]
];


Além disso, agiremos aproximadamente da seguinte forma:



    {
       = [    ] *  
}


Assim, cada modelo precisa de sua própria matriz.



Da mesma forma, você pode fazer uma cadeia de alguns objetos. Se um pássaro tiver que bater suas asas, será apropriado traduzir as coordenadas dos pontos das asas nas coordenadas do pássaro e, em seguida, nas coordenadas mundiais globais. Será muito mais fácil adivinhar a trajetória da asa imediatamente em coordenadas globais. Mas é assim, aliás.




Em seguida, você precisa decidir de que lado olharemos o mundo. Eu preciso de uma câmera.







Uma câmera é uma abstração, como uma imitação de uma câmera física. Ele tem coordenadas e alguns ângulos de inclinação que definem sua localização em coordenadas globais. Nossa tarefa é transformar um conjunto de coordenadas agora globais em um sistema de coordenadas de câmera. O princípio é o mesmo do exemplo anterior:



     {
        = [   ] *  
}


, , . !


Vejamos a cena do local onde nossa câmera condicional está:







Agora, tendo convertido todos os pontos para o sistema de coordenadas da câmera, podemos simplesmente descartar o eixo Z e interpretar os eixos X e Y como "horizontal" e "vertical". Se você desenhar todos os pontos dos modelos na tela, obterá uma imagem, como no exemplo - sem perspectiva e é difícil entender qual parte da cena realmente cai no quadro. A câmera parece ter um tamanho infinito em todas as direções. Podemos de alguma forma ajustar tudo para que o que precisamos caiba na tela, mas seria bom ter uma maneira universal de determinar qual parte da cena cairá no campo de visão da câmera e qual não.



Com câmeras físicas, podemos falar sobre algo como um ângulo de visão. Por que não adicioná-lo aqui também?



Para isso, precisamos de outra matriz - a matriz de projeção. Em geral, pode ser construído de diferentes maneiras. Dependendo do que é considerado os parâmetros iniciais, você obtém uma visão ligeiramente diferente dessa mesma matriz, mas a essência será a mesma. Usaremos a seguinte versão ligeiramente simplificada:



//     90 
const s = 1 / (Math.tan(90 * Math.PI / 360));
const n = 0.001;
const f = 10;

const projectionMatrix  = [
    [s, 0, 0,          0],
    [0, s, 0,          0],
    [0, 0, -(f)/(f-n), -f*n/(f-n)],
    [0, 0, -1,         0]
];


A matriz de projeção, de uma forma ou de outra, contém três parâmetros - este é o ângulo de visão, bem como a distância mínima e máxima para os pontos com os quais você trabalha. Ele pode ser expresso de maneiras diferentes, pode ser usado de maneiras diferentes, mas esses parâmetros estarão nesta matriz de qualquer maneira.



Eu entendo que nunca é óbvio por que a matriz é exatamente assim, mas para derivá-la com explicações, você precisa de fórmulas para 2 a 3 páginas. Isso mais uma vez nos traz de volta à ideia de que é útil abstrair - podemos operar com um resultado mais geral, sem entrar em pequenos detalhes onde não é necessário resolver um problema específico.



Agora, fazendo as transformações já familiares:



       {
      = [   ] *   
}


Obtemos em nosso campo de visão exatamente o que esperamos. Aumentando o ângulo - vemos principalmente nas laterais, diminuindo o ângulo - vemos apenas o que está mais próximo da direção para onde a câmera está direcionada. LUCRO!







Mas na verdade não. Esquecemos a perspectiva. Uma imagem sem esperança é necessária em alguns lugares, então você precisa adicioná-la de alguma forma. E aqui, de repente, não precisamos de matrizes. A tarefa parece muito difícil, mas é resolvida pela divisão banal das coordenadas X e Y por W para cada ponto:







* Aqui, deslocamos a câmera para o lado e adicionamos linhas paralelas "no chão" para deixar mais claro onde essa mesma perspectiva aparece.



Ao escolher os coeficientes a nosso gosto, teremos diferentes opções de perspectiva. De certa forma, os coeficientes aqui determinam o tipo de lente, o quanto ela “aplana” o espaço circundante.


Agora temos uma imagem completa. Você pode pegar as coordenadas X e Y de cada ponto e desenhá-las na tela da maneira que desejar.



Em geral, isso é o suficiente para construir uma cena, mas em projetos reais você também pode encontrar uma transformação adicional associada ao dimensionamento no final. A ideia é que após a projeção tenhamos as coordenadas (x, y) dentro de 1, as coordenadas normalizadas, e então as multipliquemos pelo tamanho da tela ou canvas, obtendo as coordenadas a serem exibidas na tela. Essa etapa extra tira o tamanho da tela de todos os cálculos, deixando-o apenas no final. Isso às vezes é conveniente.



Aqui, você provavelmente está com dor de cabeça com a quantidade de informações, então vamos desacelerar e repetir todas as transformações em um só lugar:







Se você combinar essas transformações em uma, terá um pequeno motor.



Qual é a aparência do Three.js?



Agora que entendemos de onde veio esse pequeno motor, vamos dar uma olhada em um exemplo do sombreador de vértice padrão em Three.js que "não faz nada":



void main() {
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}


ou mais completamente:



void main() {
    gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
}


Ele te lembra de alguma coisa? Sim, este é este motor específico. E por "não faz nada" queremos dizer que ele apenas faz todo o trabalho de recalcular as coordenadas, com base em matrizes cuidadosamente passadas de Three.js. Mas ninguém se preocupa em fazer essas matrizes com as próprias mãos, certo?



Tipos de câmera em computação gráfica e Three.js



O tópico de tipos de câmera não está diretamente relacionado a matrizes, mas ainda assim vamos dedicar alguns minutos a ele, já que ainda estamos falando sobre Three.js, e às vezes as pessoas ficam confusas com isso.



A câmera é uma abstração. Ajuda-nos a pensar no mundo 3D da mesma forma que pensamos no mundo real. Como dissemos, uma câmera tem uma posição no espaço, uma direção para a qual está olhando e um ângulo de visão. Tudo isso é especificado usando duas matrizes e, possivelmente, divisão adicional de coordenadas para criar perspectiva.



Na computação gráfica, temos dois tipos de câmeras - "com perspectiva" e "sem perspectiva". São dois tipos fundamentalmente diferentes de câmeras em termos técnicos, exigindo ações diferentes para obter uma imagem. E isso é tudo. Não há mais nada. Todo o resto são combinações, algumas abstrações mais complexas. Por exemplo, Three.js tem uma câmera estéreo - não é algum tipo de câmera de "tipo especial" em termos técnicos, mas apenas uma abstração - duas câmeras ligeiramente espaçadas no espaço e localizadas em um ângulo:







para cada metade da tela, pegamos nossa própria câmera e descobrimos imagem estéreo. E CubeCamera são 6 câmeras comuns localizadas em lados diferentes de um ponto, nada mais.



Qual é o próximo?



A próxima etapa, após obter as coordenadas dos objetos, é determinar quais objetos ficarão visíveis e quais ficarão escondidos atrás de outros. No contexto do WebGL, o navegador fará isso sozinho. Bem, ainda haverá tarefas relacionadas, como aplicar texturas a eles, calcular a iluminação por normais, sombras, pós-processamento de imagens, etc. Mas já fizemos a parte mais importante e difícil de entender. É ótimo. Na verdade, muitas coisas geradoras não precisam dessas mesmas texturas e iluminação, então pode muito bem ser que o conhecimento adquirido agora seja suficiente para trabalhar com elas.



A propósito, projetar uma sombra de um objeto em um plano nada mais é do que uma projeção desse objeto neste mesmo plano em um determinado ângulo, seguido pela mistura de cores. O processo é inerentemente muito semelhante ao da câmera, mas também adiciona um ângulo entre o plano de projeção e a "direção de visualização".


Sobre texturas e efeitos para imagens no WebGL, inclusive sem bibliotecas, sobre os quais falamos em artigos anteriores mais de uma vez. Você pode consultá-los se estiver interessado neste tópico. Assim, combinando todo esse conhecimento, seremos capazes de construir coisas 3D coloridas completas com nossas próprias mãos.



3D- . – - . , Three.js . , , , - , - . , .




Agora é a hora de resumir o que foi dito acima, para que haja espaço em sua cabeça para o próximo caso de uso de matrizes.



Então:



  • Você pode construir um mundo 3D e calcular as coordenadas dos objetos na tela com suas próprias mãos usando um trem de matrizes.
  • No mundo 3D, operamos com uma abstração como uma “câmera”. Possui localização, direção e ângulo de visão. Tudo isso é definido usando as mesmas matrizes. E há duas visualizações básicas de câmera - perspectiva e não perspectiva.
  • No contexto do WebGL, a renderização manual de uma imagem na tela ou cálculos físicos podem frequentemente remover dependências pesadas e acelerar o carregamento da página. Mas é importante encontrar um equilíbrio entre seus scripts, ferramentas prontas e opções alternativas para resolver problemas, prestando atenção não apenas à sua conveniência, mas também às questões de velocidade de download e desempenho final, inclusive nos telefones.


III. Filtros para imagens



Finalmente, veremos essa área de aplicação de matrizes como filtros para imagens. Se considerarmos uma cor no formato RGBA como um vetor, então podemos assumir que aqui podemos aplicar uma transformação semelhante à que usamos com as coordenadas:







E aplicar isso à imagem de acordo com o princípio óbvio:



    {
       = [  ] *   
}


Se a matriz identidade atuar como uma matriz, nada mudará, já sabemos disso. O que acontece se você aplicar filtros semelhantes às transformações de conversão e escala?







OU. O resultado são filtros de brilho e contraste. Interessante.



Ao fazer experiências com esses filtros, você deve sempre se lembrar de ajustar os valores para que a imagem não seja superexposta. Se você estiver multiplicando algo por um grande número, provavelmente precisará subtrair ou dividir algo em algum lugar. Conforme mostrado no exemplo anterior.


Mas como fazer uma imagem em preto e branco a partir de uma colorida? A primeira coisa que pode vir à mente é adicionar os valores do canal RGB, dividir por 3 e usar o valor resultante para todos os três canais. No formato de matriz, será mais ou menos assim:







E embora tenhamos obtido uma imagem em preto e branco, ela ainda pode ser melhorada. Nossos olhos percebem a luminosidade de cores diferentes de maneira diferente. E para transmitir isso de alguma forma durante a dessaturação, criamos coeficientes diferentes para cada canal RGB nesta matriz.



O exemplo a seguir apresentará os valores geralmente aceitos para esses coeficientes, mas ninguém se preocupa em brincar com eles. No total, esses coeficientes devem ser 1, mas dependendo de suas proporções, obteremos imagens em preto e branco ligeiramente diferentes. Isso pode, até certo ponto, simular renderizações de cores diferentes ao trabalhar com câmeras de filme.



E se também multiplicarmos um pouco a diagonal principal, teremos um filtro de saturação universal:







Funciona em ambas as direções - tanto em dessaturação (você pode chegar a uma imagem totalmente em preto e branco), e em saturação. Tudo depende do coeficiente correspondente.



Em geral, você pode brincar com filtros por um longo tempo, obtendo uma variedade de resultados:







* As matrizes usadas neste exemplo podem ser visualizadas no GitHubse você de repente precisar deles. Para serem inseridos no artigo, seu volume será excessivo.



Mas ainda vamos prestar um pouco de atenção a onde isso realmente se aplica. É claro que a própria ideia de substituir a cor de cada pixel sugere shaders para processar uma foto ou para pós-processar alguma cena 3D, mas talvez ainda esteja em algum lugar no front-end?



Filtros em CSS



Em CSS, temos uma propriedade de filtro. E aí, em particular, existem essas opções de filtros relacionados a cores:



  • brilho (nós o fizemos)
  • contraste (feito)
  • inverter (o mesmo que o contraste, apenas os coeficientes diagonais principais com um sinal diferente)
  • saturar (pronto)
  • tons de cinza (como já foi observado, este é um caso especial de saturação)
  • sépia (um conceito muito vago, diferentes versões de sépia são obtidas brincando com coeficientes, onde de alguma forma reduzimos a presença de azul)


E esses filtros aceitam coeficientes como entrada, que são então substituídos de uma forma ou de outra nas matrizes que fizemos anteriormente. Agora sabemos como essa magia funciona por dentro. E agora está claro como esses filtros são combinados nas entranhas do interpretador CSS, porque tudo aqui é construído de acordo com o mesmo princípio das coordenadas: multiplique matrizes - adicione efeitos. É verdade que não há nenhuma matriz de função personalizada nesta propriedade em CSS. Mas é em SVG!



Matrizes de filtro em SVG



Dentro de SVG, temos feColorMatrix, que é usado para criar filtros para imagens. E aqui já temos total liberdade - podemos fazer uma matriz ao nosso gosto. A sintaxe é mais ou menos assim:



<filter id=’my-color-filter’>
    <feColorMatrix in=’SourceGraphics’
        type=’matrix’,
        values=’1 0 0 0 0
                0 1 0 0 0
                0 0 1 0 0
                0 0 0 1 0
                0 0 0 0 1‘
    />
</filter>


Você também pode aplicar filtros SVG a elementos DOM regulares dentro de CSS, há uma função de url especial para isso ... Mas eu não disse isso!



Na verdade, os filtros SVG dentro do CSS ainda não são suportados por todos os navegadores (sem apontar o dedo para o IE), mas há rumores de que o Edge está finalmente mudando para um mecanismo de cromo e as versões mais antigas perderão o suporte em um futuro próximo, então é hora dessa tecnologia mestre, você pode fazer muitas coisas interessantes com ele.


O que mais acontece?



Além dos efeitos para fotos, construídos no princípio das transformações, existem várias coisas construídas sobre deslocamentos de pixel, misturando suas cores e outras manipulações, onde uma matriz pode ser um bom formato para armazenamento de dados, segundo os quais essa mesma manipulação deve ocorrer.



Matriz de kernel



Em particular, no frontend, encontramos algo como a matriz do kernel e seus efeitos associados. O ponto é simples - há uma matriz quadrada, geralmente 3x3 ou 5x5, embora possa haver mais, e os coeficientes são armazenados nela. No centro da matriz - para o pixel "atual", ao redor do centro - para pixels vizinhos. Se a matriz for 5x5, outra camada aparecerá ao redor do centro - para pixels localizados um do atual. Se 7x7 - então outra camada, etc. Em outras palavras, consideramos a matriz como um campo bidimensional, no qual você pode organizar os coeficientes a seu critério, já sem referência a quaisquer equações. E eles serão interpretados da seguinte forma:



    {
       =
           ,      
}


Uma tela em branco não é muito adequada para essas tarefas, mas os shaders são muito uniformes. Mas é fácil adivinhar que quanto maior a matriz, mais pixels vizinhos usaremos. Se a matriz for 3x3, adicionaremos 9 cores, se 5x5 - 25, se 7x7 - 49, etc. Mais operações - mais carga no processador ou placa de vídeo. Isso afetará inevitavelmente o desempenho da página como um todo.



Sempre que possível, use pequenas matrizes para esses efeitos se precisar sobrepô-las em algum lugar em tempo real.


No SVG, temos uma tag feConvolveMatrix especial, feita apenas para criar esses efeitos:



<filter id=’my-image-filter’>
    <feConvolveMatrix
        kernelMatrix=’0 0 0
                      0 1 0
                      0 0 0’
    />
</filter>


Aqui, fizemos um filtro simples para a imagem que não faz nada - a nova cor para cada pixel será igual à atual multiplicada por 1, e os valores das cores dos pixels vizinhos serão multiplicados por 0.



Observe que diferentes navegadores renderizam SVGs de forma diferente, e a renderização de cores também pode variar amplamente. Às vezes, a diferença é simplesmente catastrófica. Portanto, sempre teste seus filtros SVG ou use o canvas, que é mais previsível em nosso contexto.


Se começarmos a organizar os números em camadas, da maior para a menor, obteremos desfoque:







quanto maior a matriz, quanto mais pixels próximos tocamos, mais forte a imagem se torna. O principal aqui é não esquecer de normalizar os valores, caso contrário a imagem só acenderá.



Agora, sabendo como funciona o desfoque, podemos entender por que seu uso ativo na página dentro de CSS ou SVG leva a freios - para cada pixel, o navegador faz uma série de cálculos.


Se você começar a experimentar alterar os sinais dos coeficientes e organizá-los em padrões diferentes, obterá efeitos de nitidez, detecção de bordas e alguns outros. Tente brincar com eles você mesmo. Isso pode ser útil.







Assim, você pode fazer diferentes efeitos para fotos, ou até mesmo vídeos, em tempo real, e torná-los dependentes de alguma ação do usuário. Tudo depende da sua imaginação.



Subtotais



Vamos resumir o que foi dito nesta parte:



  • As matrizes podem ser usadas não apenas para transformações relacionadas a coordenadas, mas também para criar filtros de cores. Tudo é feito de acordo com o mesmo princípio.
  • As matrizes podem ser usadas como um armazenamento 2D conveniente para alguns dados, incluindo diferentes coeficientes para efeitos visuais.


Conclusão



Se abstrairmos um pouco dos algoritmos intrincados, as matrizes se tornarão uma ferramenta acessível para resolver problemas práticos. Com a ajuda deles, você pode calcular transformações geométricas com suas próprias mãos, inclusive no âmbito de CSS e SVG, construir cenas 3D, e também fazer todo tipo de filtros para fotos ou para pós-processamento de imagens no âmbito do WebGL. Todos esses tópicos geralmente vão além do frontend clássico e estão mais relacionados à computação gráfica em geral, mas mesmo que você não resolva esses problemas diretamente, conhecer os princípios de sua solução permitirá que você entenda melhor como algumas de suas ferramentas funcionam. Nunca será supérfluo.



Espero que este artigo tenha ajudado você a entender o tópico da aplicação prática de matrizes no frontend ou, pelo menos, tenha lhe dado uma base a partir da qual você pode continuar desenvolvendo. Se você acha que alguns outros tópicos relacionados à matemática ou à física merecem a mesma revisão no contexto do layout, escreva suas ideias nos comentários, talvez um dos próximos artigos os discuta.



All Articles