Conteúdo deste artigo
- 1. O conceito
- 2. A implementação
- A forma e a hélice
- O pergaminho
- A interação
- O material
- O pano de fundo desfocado
- Transmissão + Refração
- Fresnel + Iridescência
- Sombras de contato
- 3. O Refinamento
- 4. A acessibilidade
- Resumo
- Créditos
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:
- renderiza o fundo por conta própria, em seu próprio alvo
- desfoca esse fundo em um segundo alvo
- renderiza a cena, com as barras lendo esse fundo desfocado enquanto desenham
- renderiza um buffer normal para as barras
- 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_tintWrape observe quanto da personalidade dessa coisa reside naquele número.
Créditos
- Inspiração original: @curllmooha no X
- Efeito sonoro de freesound_community de Pixabay
- Simulação de fluidos adaptada do experimento de fluidos WebGL de Pavel Dobryakov
