Conteúdo deste artigo
- Desestruturando todo o efeito
- O que usaremos
- Uma observação sobre três partidas
- Configuração do projeto
- Adicionando nosso icosaedro base
- Criando os rostos
- … e extrusá-los
- Animar as instâncias com ruído
- Antes de trabalhar nas cores
- Cores, cores, cores
- Adicionando alguma interação
- Brincando com escalas
- Adicionando o toque final de cor
- Pós-processamento
- Palavras finais
- Créditos
Nota do editor: Enquanto fazemos a contagem regressiva para a primeira Conferência Three.js em Paris em setembro, continuamos nossa celebração da comunidade Three.js com Francesco Michelini. Neste excelente tutorial, Francesco nos conduz passo a passo pelo processo de transformar um icosaedro simples em um cluster 3D dinâmico e interativo, explorando Three.js, TSL, WebGPU e three-start ao longo do caminho.
🎟️ Ainda sem ingresso? Como parte de nossa parceria com a primeira Conferência Three.js, os leitores do Codrops podem usar o código CODROPS para obter 15% de desconto . Obtenha seu ingresso →
Não é incomum eu me encontrar em alguma plataforma de mídia social em busca de inspiração, tanto para projetos de meus clientes quanto para possíveis experimentos.
Este tutorial existe por causa disso: uma postagem no Instagram que descobri há algum tempo que acabou se tornando uma visualização interativa.
Isenção de responsabilidade : este tutorial vai direto ao ponto, então não cobrirei todo o Three.js ou qualquer configuração específica do bundler. Vou deixar isso para você!
Desestruturando todo o efeito
Primeiro de tudo, vamos definir o que o efeito contém:
- Um icosaedro giratório cujas faces são extrudadas
- Essas faces são ampliadas e diminuídas (usaremos uma função de ruído integrada)
- Um efeito de pós-processamento de pontilhamento
Isso é tudo! E o bom é que 2 dessas 3 partes já estão incluídas no Three.js.
O que usaremos
Uma observação sobre três partidas
three-start é uma biblioteca totalmente nova que ajuda a iniciar um projeto Three.js com muito poucas linhas de código e gerencia todas as coisas básicas automaticamente, como o loop de renderização, configuração do renderizador e da câmera, eventos de redimensionamento e assim por diante.
O que mais gosto nele é que ele permite dividir um aplicativo Three.js em módulos e componentes.
Um módulo é um “recurso global” que sempre é executado em todo o aplicativo; carregamento de ativos, física, gerenciamento de entradas, tudo isso poderia ser incluído em um módulo.
Componentes, por outro lado, são basicamente comportamentos que você pode anexar a cada Object3D.
Por exemplo, você pode criar um componente Spin que aceita um axis e um speed e em cada gancho do ciclo de vida de atualização desse componente, você pode girar o objeto nesse eixo dependendo da velocidade.
Além disso, você pode adicionar este componente várias vezes caso precise fazer o objeto girar em vários eixos em velocidades diferentes.
Chega de conversa, vamos começar a construir!
Configuração do projeto
Primeiro, vamos instalar os pacotes que precisamos:
###PRE_48d47a298ea5fb4a9d6c1e50caf27435###
Em seguida, crie o HTML básico:
###PRE_48fd057b7c712c923a6cc8794c08c5be###
Finalmente, em nosso arquivo /src/main.js, inicialize uma cena Three.js básica:
###PRE_eb9a70cb27ec96ca9e85d6fdf49a4447###
É isso. Agora, se você abrir o navegador, deverá ver… bem… nada. Apenas preto.
Mas se você abrir o console, deverá ver muitas mensagens update. Isso significa que tudo funciona!
Adicionando nosso icosaedro base
Primeiro, vamos criar nosso material em materials/Inner.js apenas para manter as coisas organizadas.
###PRE_0d16ff389b421e6601ee443b0465b945###
Observe : estamos renderizando as faces posteriores com side: BackSide de propósito, mas voltaremos a isso mais tarde. Além disso, estamos usando MeshNormalNodeMaterial apenas como uma solução temporária para verificar se tudo funciona conforme o esperado.
Depois disso, prossiga adicionando nossa malha em main.js.
###PRE_e6f9212960935f00def2bb62d795abae###
Voilà, aqui está nosso icosaedro:
Antes de prosseguir, vamos adicionar um comportamento Spin usando three-start. Vamos declará-lo em behaviors/Spin.js
###PRE_9f56a212410f5157a4102c24eae3061e###
Como você pode ver, criar um novo comportamento não é grande coisa: você começa estendendo a classe Object3DBehaviour e então pode gerenciar tudo por meio dos ganchos do ciclo de vida de cada componente.
Finalmente, adicione o comportamento Spin ao Icosaedro em main.js.
###PRE_c9a8ac6be8ae65a53adc83451c5acc38###
É isso. Se você fez tudo corretamente, deverá ver a malha girando.
Observe : se algo parecer visualmente “errado”, isso é totalmente normal porque estamos renderizando as faces internas da malha.
Criando os rostos
É aqui que a diversão começa. Vamos agora criar todos os rostos. Usaremos um BatchedMesh para essa finalidade.
Primeiro, aqui está o material que usaremos.
###PRE_14968d136b8e95ee9fb84e13fcfc6f2f###
Então, a malha real:
###PRE_7bde807d3aa922072113dce6cccea8d7###
O código está bem comentado, mas aqui vão os destaques:
- Crie um novo
BatchedMesh. - Faça um loop por todos os vértices, 9 por iteração, já que cada face é composta por 3 vértices. Em cada iteração, obtenha todos os vértices e construa um novo
BufferGeometry. - Calcule as normais para cada geometria.
- Adicione a geometria ao
BatchedMeshcriado anteriormente. - Feito isso, adicione o
BatchedMeshà malha principal.
Veja como está agora:
… e extrusá-los
Infelizmente, Three.js não oferece uma maneira de extrudar uma face fora da caixa, então teremos que fazer isso manualmente.
Na última etapa, criamos um novo BufferGeometry para cada face, então vamos estender esta etapa simplesmente adicionando mais vértices a essas geometrias, transladando-as para fora ao longo do faceDirection vetor.
Na última etapa, definimos 3 vértices.
-
x1, y1, z1 -
x2, y2, z2 -
x3, y3, z3
Então, a partir desses três vértices criaremos três vértices adicionais:
-
x4, y4, z4 -
x5, y5, z5 -
x6, y6, z6
…mas vamos extrudá-los um pouco. Se não estiver claro, uma imagem vale mais que mil palavras:
###PRE_01b1b1db12517cc0c7c9240688f78f01###
Agora, aqui está o resultado:
Tendo esses 6 vértices conseguimos construir cada triângulo que constrói cada instância de BatchedMesh.
Bom! Agora está tudo no lugar (quase). Vamos nos concentrar na parte visual.
Animar as instâncias com ruído
Agora, vá para materials/Cluster.js e edite desta forma:
###PRE_0e6920646056c5f8bdf1dcb0a94ade39###
Aqui estamos gerando um valor de ruído usando o centróide de cada instância mais o tempo como valor inicial. Este valor de ruído, que vai de -1 a 1 por padrão, é então remapeado para o 0.15 - 0.75 alcance, dando esse movimento orgânico.
Antes de trabalhar nas cores
Primeiro de tudo, podemos ocultar a malha interna atualizando o InnerMaterial:
###PRE_c25ef90ad929d007bc722532f9f5892a###
Definimos colorWrite: false porque não queremos que a malha seja desenhada na cena, MAS ainda precisamos dela na cena para a parte final do tutorial, então esta é a maneira mais ideal.
Além disso, depthWrite: false é necessário porque, caso contrário, você verá muitas falhas na parte traseira da malha devido ao z-fighttg.
Aqui está a aparência da cena agora:
À primeira vista, pode-se supor que falta um rosto nas instâncias, mas não é o caso.
O erro está na ordem em que definimos os vértices daquela face específica, mas o bom é que a solução é muito fácil: basta trocar dois vértices, não importa quais.
Isso ocorre porque a frente e o verso de uma face dependem da ordem em que os vértices são declarados: se forem definidos no sentido horário, a face aponta para dentro; se forem definidos no sentido anti-horário, a face aponta para fora.
###PRE_27dc669ec579f705fca7c247f67e1afc###
Cores, cores, cores
Este é fácil. Todas as instâncias têm basicamente três cores:
- Branco para as faces que apontam para o centro
- Cinza(ish) para as faces laterais
- Preto para as faces externas
Podemos gerenciar a cor de cada face usando o produto escalar. Não vou copiar e colar a definição do produto escalar, mas aqui está o que ele faz:
- Quando dois vetores normalizados se enfrentam, ele retorna
1 - Quando são perpendiculares, retorna
0 - Quando apontam para a mesma direção, retorna
-1
Diante disso, podemos calcular o produto escalar entre cada instância normal e um vetor que aponta da instância da malha para o centro, remapeá-lo para o intervalo [0, 1] e usar este valor em um mix função para obter a cor final:
Mas antes de fazer isso, devemos alterar o tipo do nosso ClusterMaterial de MeshNormalNodeMaterial para MeshBasicNodeMaterial, caso contrário não poderemos alterar a cor de saída.
###PRE_61d814a1a0650c85624eec029ea8fe98###
Em seguida, importe os novos módulos que precisamos de three/tsl e use-os para calcular as cores.
###PRE_40f3e5e1c84b97155a0e3c6a397a7ca4###
E aqui está o resultado:
Adicionando alguma interação
Lembra que estamos renderizando o InnerMaterial nas faces traseiras? Deixe-me dizer por quê.
Para esta interação, vamos disparar um raio do ponteiro do mouse na cena e então verificar se estamos atingindo a malha interna. Graças ao fato de estarmos renderizando apenas as faces internas, o raio será capaz de atingi-las apenas.
Vamos começar escondendo nosso facesMesh:
###PRE_8aa582553a3b70b057fc9cbbc3cc44e1###
…e editando o InnerMaterial. Como fizemos antes, teremos que substituir o MeshNormalNodeMaterial por um MeshBasicNodeMaterial.
###PRE_f4404750f3c00b2b6903e63341fbf719###
Se você verificar a janela do navegador, tudo o que poderá ver é uma forma preta girando.
Isso significa que está funcionando.
Para a lógica de interação real, criaremos um novo componente three-start e o anexaremos à nossa malha interna.
###PRE_63af28d8aa69a536906fd0271f6b8ee7###
O componente faz o seguinte:
- No init, ele configura um novo observador de ponteiro GSAP.
- Este Observer lança um raio sempre que o ponteiro se move e verifica se ele colide com o objeto ao qual este componente está atribuído (
this.object). - Se houver uma colisão, o valor uniforme #
hoverPointWSdo material é animado usando GSAP para corresponder às coordenadas do ponto de impacto atual. - Além disso, o getter/setter na variável
#isDebugnos permite “observar” sempre que a variável muda e acionar as coisas de acordo. Neste caso, estamos definindo diferentes valores e durações de interpolação para oeffectStrengthuniforme do material dependendo do valor para o qual a variável muda.
Agora importe-o para main.js e adicione-o à malha interna.
###PRE_a3a8f7f58fd7d9671c603eaeeb5c8d44###
Se você fez tudo corretamente, você deverá ver isto:
Isso significa que está funcionando. Agora, antes de prosseguir, restaure a visibilidade dos parâmetros facesMesh e colorWrite no InnerMaterial.
###PRE_ac213009f53df84426166132a7996c9a### ###PRE_86b5ad467f2c6fe7008b6f7aed10e6b4###
Agora podemos usar a função ###CODE_3af3af3967017ac9fe79cbb4a67227c67df### que criamos no InnerMaterial dentro de nosso ClusterMaterial simplesmente importando-o.
###PRE_4bd053024923c21ba046afa80f95154c###
Brincando com escalas
Usaremos o efeito de foco de duas maneiras distintas. A primeira consiste em aumentar a escala das instâncias dependendo da intensidade do efeito hover.
Edite o ClusterMaterial desta forma:
###PRE_2386f7c6441ad1c3b1a687b60b1d4fee###
E aqui está o resultado:
Adicionando o toque final de cor
Apesar do trabalho original ser todo em preto e branco, quero que a cena tenha um pouco de cor. Para fazer isso, usaremos um matcap simples nas faces, mas apenas onde o efeito de foco tiver alguma influência.
O matcap que usei vem de deste site (Amarelo 18), mas fique à vontade para usar o seu próprio.
Depois de colocar seu arquivo matcap em seu diretório de arquivos estáticos, carregue-o de main.js.
###PRE_be1fb9df6e21c6adb6d816d092729673###
Agora, por favor, siga-me.
Quando inicializamos um material que deveria usar texturas, não podemos simplesmente configurá-los para #null ou undefined, porque isso irá desencadear um erro e, em na melhor das hipóteses, veremos uma malha preta.
Podemos resolver isso usando uma textura de espaço reservado durante a fase de inicialização e substituindo-a assim que carregarmos nossa textura final.
###PRE_90396b203b9ac08c9acbca188ebb8367###
Se você vir tudo preto novamente, mas não houver erros no console, tudo funciona.
Agora importe o mapa uniforme em main.js e atualize seu valor assim que a textura for carregada.
###PRE_3fb0c41283813a8bca108b53d7f4b978###
Agora você deve ver este queijo esférico giratório.
Por fim, refatore o colorNode desta forma para tornar o matcap visível apenas onde e quando o efeito estiver ativo:
###PRE_474bf46e11e15374f80082bdd82cc6aa###
Pós-processamento
Agora que nosso efeito está completo, vamos adicionar alguns efeitos finais de pós-processamento.
O primeiro será o efeito Sobel, que criará um contorno ao redor das malhas.
###PRE_992e735d3c7c4204313056c73591d9aa###
Agora você deve ver uma borda branca espessa ao redor das malhas:
Finalmente aplicaremos um processo de postagem de pontilhamento, e é apenas uma questão de poucas linhas de código:
###PRE_f87fb2845f0c85d1c759ad80dcf7e7a8###
Finalmente está pronto! (Observe que a qualidade do vídeo pode ocultar o efeito de pontilhamento):
Palavras finais
Finalmente chegamos ao fim. Levei muito mais tempo para escrever este tutorial do que para realmente trabalhar no efeito.
Se ainda não o fez, recomendo que experimente o TSL. A sintaxe pode ser um pouco intimidante no início, mas uma vez clicada, torna-se uma ferramenta valiosa para trabalhar, especialmente porque todos os uniformes e funções são tratados como nós que você pode importar e usar como módulos JavaScript simples.
A mesma coisa vale para três partidas. Para mim, foi uma verdadeira virada de jogo porque ajuda a dividir o código e encapsular cada comportamento, como fizemos neste tutorial.
Olá!
Créditos
- polyhop no Instagram
- ThreeJS
- três largadas
- O poderoso GSAP e sua equipe
- Abrigo Cósmico e seus matcaps
