A história de uma animação

Uma vez, o designer ligou para o front-end e pediu para fazer uma "teia de aranha" atrás do vidro embaçado. Mas então descobriu-se que essa não era uma “teia de aranha”, mas uma grade hexagonal, e não atrás do vidro, mas foi para longe, e o front-end não estava familiarizado com o WebGL, e eu tive que aprender toda a animação no processo de desenho. Esse front-end foi Yuri Artyukh ( akella ).



Yuri se dedica a escrever há muito tempo e, aos domingos, registra fluxos com análises de projetos reais. Ele não é profissional no WebGL, não faz mapas, não escreve no Web assembler, mas gosta de aprender algo novo. No FrontendConf RIT ++, Yuri contou como conduzir uma animação do layout à entrega ao cliente para que todos ficassem felizes e aprendesse o WebGL ao longo do caminho. A história vem da perspectiva da primeira pessoa e inclui: Three.js, GLSL, Canvas 2D, gráficos e um pouco de matemática.


Teia de aranha por trás do vidro embaçado


De alguma forma, sentei-me e trabalhei em um projeto importante. Em seguida, o designer do estúdio telefona, no qual gosta de efeitos especiais, e pergunta: "Você pode fazer uma teia de aranha, como se estivesse atrás de um vidro embaçado?"

Isso, é claro, descreve imediatamente todo o problema. Como se viu depois, a "teia de aranha" atrás do vidro embaçado era essa.



Esta é uma grade hexagonal, mas para o designer, por algum motivo, uma "teia de aranha". Vidro embaçado - essa grade fica distante. Dificuldades de comunicação. Imagine como é difícil ser introvertido e fazer animações? Mas sou assim e é isso que estou fazendo.

Essa "web" não se parece com uma animação, após a qual você pode escrever um relatório sobre um caso de sucesso, abrir uma startup, obter um bilhão de investimentos, ter um monte de fãs e lançar um foguete no espaço. O que é isso tudo? Uma linha marrom em um fundo branco-cinza, como se estivesse desenhada por um mouse. Mais tarde, descobriu-se que ela deveria ir além dos limites, mas mais sobre isso depois. Em geral, o codinome é "a web por trás do vidro embaçado".

No site com essa animação, havia várias outras opções para a “teia de aranha”: em um fundo cinza no topo, em um branco abaixo. Era necessário torná-lo interativo para responder ao movimento do mouse do usuário.

A primeira coisa que os designers me perguntaram é quão difícil e quanto vai custar. Alguns pensamentos passaram pela minha cabeça: como desenhar uma linha e uma grade assim, como fazê-la não desacelerar, como deveria funcionar. Eu nunca encontrei isso antes. Mas, como uma pessoa envolvida no desenvolvimento, ele respondeu: " É fácil, vamos fazer isso ..."

Eu gosto de me envolver em aventuras estranhas, porque quando faço isso, geralmente sofro.

Através do sofrimento vem o crescimento. É inevitavelmente associado ao sofrimento - você não pode ser feliz com tudo, viver feliz e, ao mesmo tempo, se desenvolver profissionalmente.

Three.js


Comecei imediatamente a pensar em como resolver o problema. Como tudo estava em 3D, lembrei-me do Three.js . Esta é a biblioteca mais popular mencionada em todas as conferências. Essa biblioteca torna o WebGL mais simples, mais conveniente e mais agradável do que apenas o WebGL nativo.

O Three.js possui muitos objetos prontos. PlaneGeometry é o primeiro objeto que me pareceu perfeito. Este é um plano primitivo. A biblioteca possui todos os tipos de hexágonos, dodecaedros, icosaedros, cilindros, mas existe um plano simples de muitos triângulos.


Existem muitos triângulos, porque preciso de ondas detalhadas - a superfície deve se preocupar.

Se você olhar dentro do Three.js, na verdade esse plano é um objeto JS simples, com uma lista de todas as coordenadas dos pontos.



No meu caso, eu tenho um plano de 50 × 50 quadrados, então eu precisava de 2601 vértices. Por que 50 × 50 = 2601? Isso é matemática da escola. A coordenada z = 0, porque o plano, y = 1, porque esta é a primeira linha de vértices de 50 peças, e x muda.

Mas por que preciso de um avião, preciso dobrá-lo de alguma forma? A primeira coisa que você pode fazer com uma matriz é executar operações matemáticas nela. Por exemplo, percorra o for each loop e atribua a coordenada z ao valor senoidal da coordenada x.



Aconteceu algo semelhante a uma onda senoidal, porque o valor da altura de cada vértice será igual ao seno desse valor ao longo do eixo x. Para complicar isso de alguma forma (agora será um momento matemático difícil, prepare-se) - acrescentarei tempo ao seno e essa tela se moverá, simplesmente porque a matemática funciona dessa maneira. Se você adicionar tempo à coordenada x, o gráfico se moverá horizontalmente. Se coordenar y - se moverá verticalmente. Eu estava interessado em movimento horizontal - queria que meu oceano se preocupasse.

O designer não esperava que eu tivesse um senoide que se arrasta da esquerda para a direita. Ele queria que fosse bonito, como uma teia de aranha, um oceano ou o que quer que estivesse em sua cabeça. Portanto, a opção com um senoide não se encaixava. Demorou algum tipo de aleatoriedade. O avião não deveria ter sido previsível como uma onda senoidal. Mas se você chamar aleatoriamente para cada um desses picos, obteremos a própria imprevisibilidade.

A essência da aleatoriedade é que é aleatória e independente. Cada vértice aleatório não depende de seus vizinhos de forma alguma. Aleatoriamente, nenhum parâmetro é passado, ele não se importa com os vizinhos.



O resultado é uma curva quebrada que se assemelha ao oceano ou à web apenas remotamente. Mais adequado como ilustração para um filme sobre "hackers" e arrombamentos cibernéticos.

Se você olhar aleatoriamente do ponto de vista de cada ponto da tela, parecerá ruído branco - muitos pequenos pontos brancos e pretos. Cada ponto é preto e branco - 0 ou 1.

Mas o que eu preciso para criar um oceano de ondas deve ficar assim.


Assemelha-se a nevoeiro, nuvens e montanhas.

Existem funções aleatórias que retornam essas imagens. Eles são chamados de ruídos ou ruído : ruído simples, ruído Perlin. Perlin em nome do ruído é o nome do criador do algoritmo de ruído do gradiente, que retorna um belo aleatório. Ele o criou trabalhando nos efeitos especiais da primeira parte do filme "Tron". Esse algoritmo matemático já existia antes, mas agora é usado ativamente em filmes e jogos.

Quando mapas aleatórios são gerados em "Heroes of Might and Magic III" (para maiores de 30 anos) ou em estratégias., Geralmente você pode ver algo semelhante. É sempre a mesma função que retorna esses ruídos.

Existe todo um movimento "arte generativa". Os participantes geram obras de arte, paisagens, usando a função de ruído. Por exemplo, na figura abaixo, uma paisagem pseudo-natural de um dos artistas. Não está claro imediatamente se é matemática ou a topografia de uma montanha. A tarefa da arte Generativa é precisamente gerar matematicamente uma paisagem indistinguível do presente.



Verificou-se que, diferentemente da aleatoriedade, essa função recebe parâmetros, porque os pontos devem depender dos vizinhos mais próximos. Se você usar a função de ruído em vez da aleatoriedade usual, obterá algo assim.


Preto e branco é simplesmente altura: 0 são vales pretos, 1 são picos brancos. Acontece uma superfície ondulada.

Esta função está em todos os PLs porque é apenas um algoritmo - senos, cossenos, multiplicação.

Eu posso fazer a distorção da mesma maneira, percorrendo todos os vértices do meu objeto PlaneGeometry, atribuindo cada valor à função de ruído:

 geometry.vertices.forEach(v => { vz = noise(vx, vy, time); }); 

A função ocupa apenas 30-40 linhas, mas é matematicamente complexa.

Existem funções de ruído de todas as dimensões: unidimensional, bidimensional e tridimensional. No meu caso, isso é ruído tridimensional, porque três parâmetros são passados ​​para ele. Além das coordenadas espaciais dos planos x e y, transmito tempo - a superfície se torce constantemente, muda de posição.

Three.js! == GPU


Quando iniciei o algoritmo, as ondas começaram a se mover. Quando faço algo para a Web, sempre olho no criador de perfil e agora também procurei.É assim que as ondas estavam lá.



Na tela, há um quadro desenhado pelo navegador. Os quadros são indicados por linhas tracejadas cinzas verticais. Dentro do quadro, 2/3 do tempo é ocupado pela execução da função de ruído. Quando você anima algo na Web, usa o quadro de animação de solicitação, que é executado a cada 16 ms, na melhor das hipóteses. O quadro a cada 16 ms conta a função de ruído para 2600 vértices. Para cada vértice, são considerados um movimento para cima e para baixo e a altura. Em cada quadro subsequente, os valores são recalculados, porque a superfície deve viver no tempo.

Acontece que a função de ruído, que girou 2600 vezes, já ocupa 2/3 do quadro no meu computador. E este não é o quadro todo. Ao desenvolver animações, isso já é uma bandeira vermelha.

Nenhuma animação deve ocupar mais da metade do quadro.

Se houver mais, existe um alto risco de perder o quadro durante qualquer interação, qualquer botão, qualquer passagem do mouse.

Portanto, era uma bandeira vermelha dura. Percebi que o Three.js não é necessariamente WebGL. Apesar de parecer usar o Three.js, desenhei tudo em 3D, foi renderizado no WebGL, não obtive um desempenho fantástico do WebGL. Eu tenho apenas 2.600 vértices - isso não é suficiente para o WebGL. Por exemplo, em cada mapa existem milhares de objetos, cada um consistindo em dezenas de triângulos. Estime a escala: centenas de milhares são normais, mas existem apenas 2600 picos.

Vertex Shader


Após um problema com os quadros, descobri que existem shaders. Existem apenas dois tipos deles:

  • Vertex Shader;
  • Shader de fragmento.

Eu estava interessado no vertex shader - Vertex Shader. Se reescrevermos a animação, ela ficará assim:

 position.z = noise( vec3(position.x, position.y, time) ); 

Position.z - coordenadas z do componente de cada ponto com seus próprios tipos de dados. vec3 indica que haverá três parâmetros.

Não há loop no shader.

Antes disso, no script, coloquei um for each loop e, para cada vértice, os cálculos ocorreram em um loop. A diferença entre shaders e não shaders é a ausência de um ciclo.

Shader - este é o ciclo.

É executado em paralelo para todos os vértices de uma só vez. Este é o seu principal significado, e missão, e chip e missão.

A GPU na placa de vídeo possui um núcleo maior, diferente da CPU do processador principal. No processador, há muito menos deles, mas é capaz de executar cálculos universais mais rapidamente. Cálculos muito simples estão disponíveis na placa de vídeo, mas existem muitos núcleos, permitindo paralelizar muitos cálculos. Isso é o que geralmente acontece em shaders. O significado do sombreador de vértice é que o ruído será calculado em paralelo para 2600 vértices no sombreador na placa de vídeo.

Se você olhar para o criador de perfil, a aparência da animação não será alterada, mas será parecida com esta.



Nada é executado na CPU . Obviamente, outro segmento na GPU foi adicionado abaixo. Também existem threads na GPU, CPU, trabalhadores da Web, mas esses cálculos já serão realizados em um thread separado na placa de vídeo.

Claro, isso não é gratuito. A placa de vídeo no trabalho é aquecida mais que o processador principal. Portanto, frequentemente quando você visita sites com animações semelhantes, os fãs começam a trabalhar para você. Isso ocorre porque, quando a placa de vídeo é ligada, ela requer resfriamento, ao contrário do resto do tempo. Em dispositivos móveis, isso é mais importante do que em computadores de mesa - os telefones celulares simplesmente acabam mais rápido. Mas, ao mesmo tempo, você obtém um ganho de desempenho bastante radical.


O resultado é essa superfície - esse é o ruído perlin habitual. Se você iniciar e alterar apenas o tempo, são obtidas ondas frias.

Mas isso não é tudo. Eu ainda era obrigado a "teia de aranha" - uma grade hexagonal na superfície. Tendo experiência em layout, a maneira mais simples e óbvia é selecionar um fragmento que possa ser repetido. Curiosamente, para uma grade hexagonal, ela não é quadrada, mas retangular. Se você repetir o padrão como um retângulo, obtém uma grade. A biblioteca Three.js permite sobrepor png e não aprender todo o WebGL antes disso. Eu cortei png e coloquei na superfície, ficou algo assim.



À primeira vista, o que você precisa! Mas só a princípio. Isso não me agradou, já que a animação era necessária para o site de criptomoeda - tudo deveria ser "caro, rico".

Quando você usa texturas png e elas estão próximas da câmera, você pode ver que as bordas do elemento mais próximo estão embaçadas. Não há sensação de que a imagem esteja clara. O png parece ter se estendido no navegador. O problema é que, no WebGL, não há como usar texturas de vetor no sentido pleno da palavra. Então eu chorei e li na Internet que o GLSL resolve esse problema.

GLSL é uma linguagem do tipo C na qual os shaders são escritos. Todo mundo tem medo de usá-lo, porque esses são shaders, WebGL - nada está claro! Mas descobri que é possível criar imagens nítidas e voltei para o segundo tipo de sombreador.

Tonalizador de fragmentos


Esse shader faz a mesma coisa que o vértice. Porém, se o vértice construir uma superfície de polilinha, executando cálculos para cada vértice, o sombreador de fragmento calculará a cor de cada pixel da superfície.

A função mais básica de fragment shader – step(a,b) . Retorna apenas 0 e 1:

  • se a> b, então 0;
  • se a <b, então 1.

Fiz uma pseudo-implementação em JS para deixar claro como essa função é simples.

 function step(a, b) { if (a < b) return 0 else return 1 } 

Quando você trabalha no WebGL, geralmente em qualquer objeto, existe um sistema de coordenadas. Se for um objeto quadrado, o sistema de coordenadas é primitivo: pontos (0,0), (0,1), (1,0), (1,1).

Um sombreador de fragmento é executado para cada pixel. Se o Vertex Shader for 2600 vezes por quadro, o Fragment Shader será executado quantas vezes houver pixels. Talvez um milhão de vezes para cada quadro, se a superfície for 1000 × 1000 px. Parece assustador, mas simplesmente porque poucas pessoas estão familiarizadas com os recursos das placas de vídeo em nosso tempo.

Se você usar a função step (a, b) com as coordenadas desses pixels, poderá executar a função step com o parâmetro 0.4 e passar a coordenada x de cada pixel para cada ponto.

Acontece que tudo que for menor que 0,4 será 0, tudo que for maior que 1. No WebGL, números e cores são iguais. Cada cor é um número. Branco - 1, preto - 0. Existem três deles em RGB, mas ainda são 0.0.0 e 1.1.1.



Se executarmos essa função passo mais complicado, ficaremos brancos à esquerda. Esta função será executada para cada ponto da tela e considerará que é 0 ou 1. Isso é normal, não se preocupe.

Se você multiplicar essas duas expressões, obterá uma faixa branca vertical. Se você fizer o mesmo em um eixo diferente, poderá desenhar um quadrado branco:


Deveria ser o clímax - desenhámos um quadrado branco!

Usando combinações de apenas uma função, você pode desenhar o que quiser.

Se você se lembra, os elementos do padrão estavam em ângulo. Se você criar uma superfície inclinada, que consiste apenas em cores preto e branco, ela será nervurada, não suavizada. Nervuras chamam sua atenção - é feio. Para tornar a superfície mais suave para os olhos, são necessários não apenas pixels em preto e branco, mas também meios-tons cinza.

Smoothstep


Os shaders têm uma função de passo suave. Faz o mesmo que passo, mas interpola entre 0 e 1 para que haja um gradiente.


Da esquerda para a direita, após a compactação máxima.

Se você compactar essa função o máximo possível, obterá uma linha com gradiente mínimo. É exatamente isso que você precisa para gerar uma linha perfeitamente suave em qualquer ângulo no sombreador Fragment.

Então eu pude fazer um quadrado branco com bordas suaves. Se houver um quadrado branco, você pode fazer 3 quadrados brancos.


Quadrados podem ser girados, use as funções de seno e cosseno.

Então eu tive que usar papel e uma folha para quebrar meu padrão.


Captura de tela da produção.

Lá tudo está conectado com 23 graus diferentes de multiplicidade, por isso não foi muito difícil calcular as coordenadas de todos esses pontos. E então você pode obter esse padrão.



Desenhei um fragmento e o repeti várias vezes. Você pode ver claramente o modo de depuração, onde o padrão se repete. Todos os fragmentos são realizados usando as funções paramétricas step e smoothstep . Isso significa que, ao criar um padrão para inclinar o plano, você pode gerar um número infinito desses padrões. Se dentro do fragmento alteramos a espessura da linha ou o tamanho dos hexágonos, obtemos muitos outros padrões.

Torci os parâmetros e encontrei um número infinito de padrões. É como "arte generativa" - não está claro o que foi feito, mas de maneira bonita.

SDF


Então descobri que também existem campos de distância assinados - gerando imagens com um mapa de distância . O SDF é usado em cartões ou em jogos de computador para desenhar textos e objetos, porque é ideal. No WebGL, é difícil desenhar texto de uma maneira diferente, especialmente suave e delineada.



Este é um formato matemático difícil de usar fora do WebGL. A ideia é simples, mas elegante e dá um efeito bonito.

Se quisermos desenhar uma estrela clara, precisamos salvar a imagem à direita - esta é a imagem gerada da imagem. Já existe um algoritmo que transforma qualquer imagem clara em desfocada. Depois disso, ele pode ser usado para gerar uma versão clara, mas, ao mesmo tempo, não temos uma imagem, mas muitas. A partir de uma imagem clara do mesmo tamanho, você pode gerar o mesmo, mas maior. Será com erros, mas a abordagem é matematicamente interessante.

Por exemplo, se você tirar uma foto com um tamanho de 128 × 128 px, a partir de uma foto em tamanho pequeno, poderá obter uma imagem nítida várias vezes maior que a origem. Essa é uma das razões pelas quais eles usam SDF - uma fonte borrada geralmente pesa menos do que em um formato vetorial otimizado.

Claro, há uma limitação. É impossível aumentar as letras para 1000 px, mesmo 100 px parecerão feios. Mas com que frequência são necessárias fontes desse tamanho?

Shader de fragmentos, desenhando retângulos, espalhados - com a ajuda dessas perturbações, finalmente consegui encontrar a superfície desejada.



Novas condições


Ela era como deveria: se contorcendo, todos os elementos estavam claros. Tudo estava como eu queria, mas aconteceu que havia mais:

- E deixe-o se mover com o mouse e um novo caminho está sendo traçado. E os favos de mel são destacados!

Supunha-se que, quando o usuário move o mouse, ele metaforicamente abre seu caminho espinhoso ao longo da "teia de aranha" quebrada usando o serviço.



Não é difícil descrever a tarefa em palavras, mas como implementá-la? A primeira coisa que pensei - como tenho uma grade hexagonal, provavelmente já foi estudada. Então me deparei com um artigo interessante “ Guia de referência e implementação de grade hexagonal ”. Nele, o autor coletou materiais por 20 anos. Ele é legal sem questionar, e o artigo é divino para quem gosta de algoritmos e matemática. Ele contém muitos dados interessantes sobre malhas hexagonais.

O artigo é longo, mas contém abordagens matemáticas interessantes: como construir um sistema de coordenadas em uma grade hexagonal, como numerar esses hexágonos e depois se referir a eles onde é usado. Acontece que isso sempre esteve diante dos meus olhos, porque em jogos antigos de computador são usadas redes hexagonais em todos os lugares.


Se você já está afinado com um traste hexagonal - olhe para o castelo. Em outras texturas, uma grade hexagonal também é adivinhada.


Em "Civilização", tudo é geralmente óbvio.

Também foi interessante saber que, se você faz uma seção transversal ao longo da diagonal de um cubo tridimensional, que consiste em muitos cubos pequenos, então, por um lado, são cubos e, por outro, hexágonos regulares.



A seção transversal de um cubo tridimensional fornece uma grade hexagonal bidimensional. Foi divertido aprender que cubos tridimensionais estão de alguma forma conectados com hexágonos bidimensionais.

No artigo, inclusive, havia um algoritmo para encontrar o caminho ao longo da grade hexagonal. Eu tive que procurar uma maneira de altura através do mouse.

Os algoritmos de busca de caminhos são complexos e simples. O mais primitivo é desenhar uma linha entre os pontos e ver em quais hexágonos essa linha se encaixa. Acontece que, antigamente, as unidades passavam do ponto A ao ponto B.


Eu precisava de algo assim.

Mas não é disso que eu preciso. Aqui o caminho é colocado ao longo das áreas dos hexágonos, e eu preciso ao longo das bordas. Eu tive que resolver o problema de maneira diferente.

Canvas2d


Talvez haja maneiras melhores, mas a minha é mais interessante. No começo, desenhei o Canvas2D para minha depuração - etapa 1.



Antes disso, havia WebGL, Three.js, shaders, e isso é apenas o Canvas2D! Eu desenhei todos os pontos da grade hexagonal nela. Se você olhar de perto, esses são os mesmos hexágonos. Então me lembrei dos gráficos que armazenam informações sobre como os pontos são conectados entre si, e os conectamos com os três vizinhos e obtemos um gráfico - passo número 2. Para isso, usei Open Source Beautiful Graphs .

Um gráfico é simplesmente uma coleção de pontos e informações sobre como eles estão conectados. Eles são bem estudados, existem muitos bons algoritmos para todos os gostos, para encontrar o caminho do ponto A ao ponto B dentro do gráfico. Todos os cartões usam esse tipo de algoritmo.

Parece algo assim.

 graph = createGraph( ); graph.addNode(..); // 1000 nodes graph.addLink(..); // 3000 links graph.pathFinder(Start, Finish); //0.01s 

Construímos um gráfico, adicionamos 1000 pontos e todas as conexões entre eles. Em seguida, passamos o id cada ponto - o primeiro está conectado ao terceiro, o terceiro ao quinto. Não há necessidade de inventar nada, existe um algoritmo otimizado com uma função pronta que permitirá que você encontre esse caminho.

Esse algoritmo executa menos de um quadro. Obviamente, é preciso algum tipo de recurso, mas você não precisa executá-lo a cada 16 ms, mas apenas quando o caminho muda.

Então, eu pude construir essa rota no passo número 3. No Canvas2D, ele começou a ficar assim: o caminho mais curto do ponto A ao ponto B - tudo, como na vida. À primeira vista, parece que este não é o caminho mais curto, mas acontece que existem muitos caminhos mais curtos do ponto A ao ponto B ao longo da grade hexagonal.

No WebGL, todas as imagens são números. Lá você pode transferir texturas no shader, por exemplo, tentei transferir png. Não há diferença para o navegador - ele é passado png ou Canvas2D. Para o navegador Canvas2D, é o mesmo que a imagem final, bitmap. Portanto, eu primeiro desenhei essa imagem na forma de uma cobra. № 4.

Canvas2D . Canvas2D , — . , . , Canvas2D 3D, , .

, , WebGL — , «, », .

, , Canvas2D, , , . Canvas2D , WebGL, .


: , , .

, --. , , «» .

?


: « ? ?» , : «, !». , . .

, WebGL. , . , WebGL. 2 — , .

, , . , .

, . , , , 3D — .

, . , , .
FrontendConf . , Canvas , RxJS JSX React .

30 . , , .

Source: https://habr.com/ru/post/pt450832/


All Articles