(85) 99645-7140 nelclassico@gmail.com Praça Coronel Melquiades, 124
Tecnologia

Construindo um xilofone de vidro interativo infinito com Three.js | Codrops

Nota do Editor: A primeira Conferência Three.js chegará a Paris em setembro, e estamos entrando no espírito dela! Nas próximas semanas, traremos a você muitas joias do Three.js de pessoas maravilhosas da comunidade, compartilhando generosamente seus experimentos, técnicas e ideias criativas conosco. Estamos começando com este lindo xilofone interativo da Sujen! Aproveitar!

🎟️Indo para Paris? 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 em ingressos . Se você estava esperando por um sinal, pode ser este. Obtenha seu ingresso

Este é um xilofone que você toca com o cursor. Passe pelas barras e cada uma toca sua nota, balança como um carrilhão e inunda-se de cor à medida que a esteira passa sobre ela.

A coluna também nunca termina. Role o quanto quiser e sempre há mais barras, sessenta e quatro delas girando eternamente em uma hélice de vidro fosco.

Sempre fui atraído por materiais que são quase translúcidos, mas ainda mantêm um pouco de brilho, o suficiente para captar a luz e permanecerem distintos. Chique, mas mínimo. Foi aqui que essa ideia surgiu.

Aqui está o que abordaremos:

  • Um pergaminho sem fim onde nada se move. A hélice inteira é disposta a partir de um único número, então as barras são recicladas de cima para baixo e a composição nunca muda.
  • Uma simulação de fluido usada como máscara. Cada compasso já possui sua cor, permanentemente, da mesma forma que uma tecla de xilofone real possui sua nota. A simulação decide quanto dessa cor você pode ver no momento.
  • Vidro fosco convincente. Por que MeshPhysicalMaterial não funciona em geometria instanciada e como falsificar cada sugestão óptica com desfoque fora da tela, refração espaço-tela, Fresnel, iridescência e oclusão de ambiente substituindo sombras de contato.
  • Animação oscilante sem estado de CPU. Sessenta e quatro barras oscilando de forma independente, orientadas por um carimbo de data/hora cada e algumas linhas de sombreador de vértice.

Ele é construído com Three.js no Vite simples, com pós-processamento executando o pipeline de passagem. Presume-se que você esteja confortável com Three.js e já tenha escrito um shader antes.

1. O conceito

Este começou com a rolagem do apocalipse. Tenho uma relação de amor e ódio com o Twitter, onde me inspiro nas grandes ideias de todos e depois me sinto um pouco soterrado pelas grandes ideias de todos. Mais alguém?

Me deparei com um post que não conseguia parar de olhar: a interação, as cores, a suavidade da animação. Então comecei a descobrir como. E para ficar mais interessante, resolvi dobrar também um xilofone tradicional.

2. A implementação

Começamos construindo a forma e a hélice, depois adicionamos o scroll e a interação, depois o material que vende a vidraça do xilofone.

A forma e a hélice

Modelei a barra como uma cápsula que lembra uma tecla de xilofone, mas arredondada, para que tenha alguma profundidade para captar a luz. Ele exporta como uma única barra em .glb e nós a instanciamos 64 vezes a partir de um InstancedBufferGeometry.

Modelei a barra como uma cápsula que lembra uma tecla de xilofone, mas arredondada, para que tenha alguma profundidade para captar a luz. Ele exporta como uma única barra em um .glb e nós a instanciamos 64 vezes a partir de um InstancedBufferGeometry.

###PRE_16c3758c1aa98ff79ecac9921319d64f###

Dispor esses 64 em uma hélice é basicamente o que você esperaria. Ande em um ângulo ao redor de um círculo, suba em Y, vire cada barra para fora. A parte que não é óbvia é a terceira rotação:

###PRE_9e78e25cbc3ede939ce561a278254133###

tiltX é o que permite que cada barra se incline para a subida em vez de ficar plana. Pense em uma escada em espiral: sem a inclinação você obtém degraus, placas niveladas empilhadas uma acima da outra. Mas com ele você consegue o corrimão, correndo suavemente ao longo da encosta. O ângulo em si é apenas subir ao longo da corrida. A subida tem a altura de uma barra, porque é o quanto subimos entre uma barra e a seguinte. A corrida é a distância que percorremos lateralmente no mesmo passo, que aumenta com o raio: uma hélice larga sobe suavemente, uma hélice estreita sobe abruptamente. Math.atan2 transforma esses dois comprimentos em um ângulo e tiltFalloff é uma configuração na parte superior para achatá-lo ou aumentá-lo.

A ordem de Euler também é importante. "YXZ" aplica a rotação global antes de adicionar a inclinação personalizada à rotação X local da barra.

O espaçamento vertical tem exatamente a mesma altura da caixa delimitadora, de modo que as barras ficam alinhadas e sem lacunas. O espaçamento angular funciona um pouco menos de duas voltas completas no 64. Todo o grupo é então inclinado e rolado de modo que a hélice entre no canto superior esquerdo e saia no canto inferior direito, e diminua para caber.

O pergaminho

A rolagem não move o grupo, a câmera ou qualquer outra coisa. A rolagem se acumula em um valor de fase única medido em unidades de índice de barras, que é facilitado e alimentado no wrap() daquele loop acima:

###PRE_e69694f2d7660ccbf3dd4b93846a0f30###

Como a fase alimenta o layout em vez de um índice, cada barra desliza continuamente para cima na espiral e, no momento em que passa pelo topo, ela reaparece na parte inferior. O envelope (qual é a altura, a largura, como fica na moldura) é fixo. Você pode rolar por dez minutos e a composição fica exatamente onde eu coloquei.

Duas pequenas coisas o mantêm barato. A atenuação usa MathUtils.damp em vez de lerp, portanto, ele se estabiliza na mesma velocidade em uma tela de 60 Hz e em uma de 120 Hz. E os buffers de instância só são reescritos quando a fase facilitada realmente mudou.

Nota: 64 barras neste passo angular farão com que a reciclagem salte a rotação, embora seja invisível porque acontece fora do quadro. Se você deseja que a reciclagem seja genuinamente contínua, escolha uma etapa que se divida igualmente em 2π.

A interação

Passei um número embaraçoso de horas pensando na cor e na velocidade do movimento, depois muitas mais apenas brincando com ele.

A cor foi a parte difícil. Eu sabia que queria uma simulação fluida, mas também queria que cada barra mantivesse sua própria cor, como um xilofone de verdade. Meu primeiro instinto foi acender toda a barra ao pairar. Funcionou, mas foi chato.

Aqui está o arranjo que fiz. Cada barra carrega um número: sua posição na linha, de 0 na parte inferior a 1 na parte superior. Este número é seu slot permanente em uma textura gradiente.

O gradiente é construído em tempo de execução em uma tela: uma rampa de prisma em vermelho, magenta, violeta, azul, ciano.

###PRE_b6764ca36ba75495b778c01fea58e2cf###

O campo de velocidade do fluido então decide quanto dele você pode ver:

###PRE_af2f567aa2b3769ae98969dab5fe9766###

Algo que vale a pena notar é que cada fragmento mostra a velocidade do fluido em sua própria posição na tela, então a revelação segue a forma do redemoinho à medida que ele flutua e decai. Em repouso, as barras ficam quase brancas. Varra e a esteira revela a cor de cada barra, nunca a do cursor.

Adicionamos uma configuração u_tintWrap, para que a rampa suba dez vezes as 64 barras em vez de uma vez. Este é o número com o qual brincar se você construir algo semelhante.

A simulação de fluidos em si é um solucionador de fluidos estáveis ​​bastante padrão, mas bastante simplificado. Ele roda em 128×128. Ele faz uma única iteração de pressão onde um solucionador real faria dezenas. Quase não há vorticidade.

A única coisa que não economizei foi respingar ao longo de um segmento em vez de em um ponto:

###PRE_9884e5cecc6f519e5586dbb9a311cfed###

Salpicar ao longo da linha da posição anterior até a atual cria uma pincelada suave.

Descobrir qual barra você está pairando é deliberadamente pouco sofisticado. Fazemos um loop sobre todos os 64 e raycast contra uma caixa delimitadora. O truque é empurrar o raio para o espaço local de cada barra em vez de transformar a caixa, o que é muito mais barato. Cada objeto temporário é alocado uma vez antecipadamente, portanto, um quadro de seleção não aloca nada.

O ataque

Quando você passa por uma barra, ela balança. A maneira óbvia de construir isso é uma interpolação por barra, marcada na CPU a cada quadro. Optei pelo caminho mais barato e animei-o no shader.

###PRE_5eeb6c9e293b41cefbea7bf59bcda14a###

A fórmula é um pêndulo amortecido, escrito em uma linha em vez de passo a passo quadro a quadro. Dê-lhe o tempo desde o ataque e ele retornará um ângulo, amplo no início e desaparecendo após cerca de um segundo. Tudo o que a CPU faz é anotar quando cada barra foi atingida, de modo que sessenta e quatro barras podem oscilar independentemente e nunca se sabe.

Há dois pequenos detalhes a serem observados aqui:

  • aStrikeTime é inicialmente definido para um tempo muito distante no passado, então o decaimento cai para 0.
  • A ordem de qmul é importante. Neste momento, a barra gira em torno do seu fim. Se invertidas, as barras orbitarão em torno do centro da hélice.

O som

Então, é claro, um xilofone precisa fazer som. Baixei uma amostra de uma única nota e mudei o tom em uma escala pentatônica, então percorrer os compassos com pressa não parece horrível.

Usamos três oitavas acima de C5, o que dá quinze notas em sessenta e quatro compassos, então aproximadamente quatro compassos vizinhos compartilham uma afinação. Isso é deliberado. Espalhar sessenta e quatro notas distintas em toda a extensão afastaria os extremos da velocidade original do sample, e um sample esticado com tanta força começa a soar como um desenho animado.

Nota: As notas são disparadas quando o cursor entra em um compasso, e não enquanto ele fica lá, o que é mais próximo de como um xilofone real se comporta.

O material

Agora a parte que deu início a tudo isso: a superfície vítrea, fosca e levemente brilhante. Este é um moodboard do material ideal que eu queria.

A abordagem óbvia é MeshPhysicalMaterial com a transmissão ativada e quero deixar claro que não a evitei propositalmente. Realmente não pode funcionar aqui. Todas as 64 barras são uma chamada de desenho de instância única, e Three.js não pode classificar em profundidade as instâncias umas contra as outras. Assim as barras ficam completamente opacas.

Fazemos algumas passadas para conseguir o material ideal. Os passes são executados nesta ordem:

  1. renderiza o fundo por conta própria, em seu próprio alvo
  2. desfoca esse fundo em um segundo alvo
  3. renderiza a cena, com as barras lendo esse fundo desfocado enquanto desenham
  4. renderiza um buffer normal para as barras
  5. oclusão de ambiente e depois antialiasing

O pano de fundo desfocado

A etapa 1 usa uma máscara de camada em vez de uma segunda cena. O quadrante de fundo reside em sua própria camada; a passagem restringe a máscara da câmera a essa camada, renderiza e depois coloca a máscara de volta.

A etapa 2 entrega o fundo nítido ao GaussianBlurPass do pós-processamento, que desfoca um alvo em outro e gerencia seus próprios buffers de rascunho. Ele funciona com um quarto da resolução da tela e executa algumas passagens sobre ela, o que proporciona uma geada ampla e leitosa sem pagar por um núcleo largo em tamanho real. Um único valor de congelamento fica em cima dele, então a força pode ser ajustada em tempo de execução.

Transmissão + Refração

E então, depois de toda essa configuração, a transmissão é uma única textura lida:

###PRE_96565cfe6c81b8d503331c341af9e9db###

Deslocar essa pesquisa pela normal da superfície é o que dobra o fundo nas bordas e vende a espessura.

O (1.0 - 0.6 * reveal) é uma coisa pequena que eu gosto. Onde quer que a esteira revele cores, deixamos menos fundo leitoso passar, para que a tonalidade permaneça vívida em vez de ser lavada pela geada.

O desfoque é realmente difícil de ver contra um fundo simples, então há uma alternância de fundo padronizado atrás do painel de ajuste somente para desenvolvedores. Nunca é enviado, mas é a primeira coisa que procuro quando a geada parece errada.

Fresnel + Iridescência

Fresnel vem em seguida, misturando a barra em direção à cor do céu em ângulos rasantes. É isso que dá definição às bordas e tira cada barra do fundo. Não há mapa de ambiente por trás dele, apenas uma rampa de três paradas do solo ao horizonte e ao céu, gerada no shader de fragmento. O brilho só é lido em ângulos rasantes, onde um HDR real é indistinguível de um gradiente, então não vale o megabyte.

As barras ainda pareciam opacas, então adicionei iridescência, uma paleta de cossenos que representa a interferência real do filme fino. Existe um termo na fase que varia com a normal da superfície, agindo como uma falsa variação de espessura para que o interior não tenha uma tonalidade plana. É barato e faz o trabalho.

Sombras de contato

Em seguida, entre em contato com as sombras, exceto que não há chão. A composição flutua no espaço, então não há nada onde a sombra possa cair. Em vez disso, as barras que ocluem umas às outras tornam-se a sombra, via SSAO.

Isso precisa de uma peça não óbvia. SSAO quer um buffer de normais de superfície, e o passe normal integrado de Three desenha com um material genérico que não sabe nada sobre nosso giro por barra e swing de golpe. Portanto, renderizamos normais usando o mesmo sombreador de vértice do material de exibição , compartilhando os mesmos objetos uniformes por referência em vez de copiar valores. As referências compartilhadas não podem se separar, portanto, o buffer normal não pode ficar fisicamente fora de sintonia com o que você vê.

Finalizamos com SMAA para limpar as bordas.

3. O Refinamento

Há muitas passagens aqui, o que não parece uma receita para performance. Então, em vez de cortar passes, tornei cada um deles barato.

A solução de fluido está inativa. Após alguns segundos sem entrada, toda a resolução é ignorada. E o transportador de rolagem reescreve seus buffers somente quando a fase facilitada realmente se moveu, então a rolagem estabilizada significa zero reescritas e zero uploads.

Depois disso, são necessárias algumas pequenas decisões. A simulação de fluido é pequena e grosseira. Uma renderização de fundo alimenta tanto a cópia nítida para refração quanto a cópia desfocada para transmissão.

Os telefones têm seu próprio nível. A proporção de pixels do dispositivo é limitada em todos os lugares. Em um dispositivo de ponteiro grosso com uma janela de visualização pequena, a tampa cai ainda mais e os dois buffers de resolução total (as normais da superfície e a passagem de oclusão) caem para metade. Esses três juntos são o que uma GPU de telefone não consegue manter a 60fps; todo o resto já era barato o suficiente para ser deixado de lado.

Os redimensionamentos são medidos a partir de um elemento dimensionador em vez da janela, e protegidos para que tamanhos idênticos não alterem os alvos de renderização.

4. A acessibilidade

As barras são o caso interessante aqui, e a correção é uma linha:

###PRE_b0cc188258c5c5d8ff83aa94db6d5484###

Multiplique o ângulo de balanço por zero e o movimento desaparece. Nada mais muda. A barra ainda acende, a nota ainda toca, a cor ainda se revela.

A jogada tentadora é bloquear toda a interação por trás de um booleano. Mas isso não dá a ninguém uma versão mais calma do instrumento; isso tira o instrumento deles. A redução do movimento deve remover o problema vestibular, não a característica.

Nota: não há rota de teclado para tocar o instrumento. Tocar uma barra precisa de um raio de ponteiro, então um usuário do teclado alterna a página e o som, mas não consegue tocar uma nota. A solução que eu procuraria são as teclas de seta percorrendo um índice de foco através das barras e disparando o mesmo caminho de ataque.

Resumo

A conclusão, que me recusei a admitir, é quanto esforço é necessário para fazer algo parecer simples.

Algumas coisas que vale a pena tentar:

  • Escolha um passo angular que se divida uniformemente em 2π para um loop genuinamente contínuo
  • Troque a pentatônica por uma escala que você mais gosta
  • Empurre u_tintWrap e observe quanto da personalidade dessa coisa reside naquele número.

Créditos

Deixe um comentário

Seu email não será publicado. Campos obrigatórios marcados com *

Este site utiliza o Akismet para reduzir spam. Saiba como seus dados em comentários são processados.