2 pontos por GN⁺ 2025-02-08 | 1 comentários | Compartilhar no WhatsApp
  • Com apenas 240 linhas de código GLSL, a animação do Rick é criada, e o shader da GPU calcula a cor de cada pixel ao longo do tempo sem usar imagens nem bibliotecas
  • O ponto central é que color_for_pixel(pixel, time) é executada para todos os pixels, e a signed distance field (SDF) é a estrutura que representa interior, exterior e contorno das formas com valores de distância
  • O rosto é montado combinando formas SDF como round_rect(), circle(), star(), bezier() e parabola() com deslocamento, escala, espelhamento, repetição e união
  • As ondulações do cabelo, a repetição dos dentes, o contorno dos lábios, o piscar, o movimento das pupilas e o balanço da cabeça são adicionados gradualmente com domain warping, mod(), sin(time) e noise()
  • A versão final inclui até o fundo do portal, mas como o editor não tem exportação de vídeo, é necessário um fluxo separado no macOS com glslviewer e ffmpeg

Estrutura do shader que calcula a cor por pixel

  • A animação é escrita em OpenGL Shading Language (GLSL)
  • É possível executar e modificar os exemplos na hora no editor de live coding da página
  • O ponto de entrada básico é a função color_for_pixel(vec2 pixel, float time)
    • A GPU executa essa função para todos os pixels da prévia
    • A pergunta que a função responde é: “que cor este pixel deve ter neste momento?”
  • Um exemplo simples coloca os valores pixel.x, pixel.y e time nos canais de cor para visualizar a posição do pixel e a variação no tempo
  • time é o número de segundos desde a última edição e continua aumentando

Como as formas são desenhadas com SDF

  • Um círculo pode ser desenhado calculando a distância do pixel até a origem com length(pixel) e comparando com o raio
  • A função circle() não retorna apenas um bool indicando dentro ou fora, mas sim a distância até a borda
    • Dentro da forma: valor negativo
    • Fora da forma: valor positivo
    • Na borda: valor próximo de 0
  • Esse tipo de função é uma função de signed distance field (SDF)
  • Com o valor de distância, preenchimento e contorno podem ser tratados da mesma forma
    • Se dist < -0.01, usa a cor interna
    • Se dist < 0.0, usa contorno preto
  • Várias formas SDF podem ser unidas com min()
    • Se a cabeça e as orelhas do Rick tiverem contornos tratados separadamente, surge uma linha desnecessária entre elas
    • Ao unir os dois valores de distância com min(), sobra apenas o contorno da união da cabeça com as orelhas
  • Existem outras formas de combinação, e os materiais de referência ligados são 2D distance functions de Inigo Quilez e conteúdos sobre combinação de primitivas

As formas que compõem o rosto do Rick

  • O formato da cabeça é feito com round_rect(), e as orelhas também são adicionadas com round_rect() separado
  • A imagem do Rick do pôster da 1ª temporada é sobreposta piscando na prévia para ajustar os valores
    • Muitos números foram encontrados nesse processo de tentativa e erro
    • Os valores de cor foram obtidos com a ferramenta conta-gotas de um editor de imagens
  • Os olhos são compostos por SDF circular e uma star() de 6 pontas
    • O globo ocular é levemente esticado na vertical com transformação de coordenadas como pixel.y *= .93
    • A pupila arredonda as pontas subtraindo um pequeno valor de distância da SDF de estrela de 6 pontas
  • Os dois olhos são feitos com espelhamento lateral usando pixel.x = abs(pixel.x), sem duplicar código
  • Nariz, boca e sobrancelhas são desenhados com bezier()
  • O cabelo começa a partir de uma star() de 11 pontas alongada verticalmente
  • A ordem do código que desenha as formas também afeta o resultado
    • A primeira cor retornada é a forma que aparece na frente
    • Dentes e língua só são desenhados dentro da condicional do formato da boca, então não saem para fora dela

Técnicas para refinar cabelo, dentes e linhas decorativas

  • O cabelo rígido em forma de estrela vira uma ondulação com domain warping
    • Domain warping é uma técnica que embaralha as coordenadas com offsets aleatórios baseados na posição do pixel
    • A mesma posição recebe o mesmo offset, permitindo uma distorção consistente ao longo do tempo
    • Como referência, há o material domain warping de Inigo Quilez
  • Os dentes são feitos criando um com a SDF parabola() e repetindo horizontalmente com mod()
    • mod(pixel.x, width) faz a coordenada x recomeçar do 0 em intervalos regulares, repetindo a mesma forma
    • pixel.y = abs(pixel.y)-.06 espelha os dentes de cima e de baixo
    • Um deslocamento em y baseado em pow(pixel.x, 2.) ajusta os dentes à curva do sorriso
    • Condições como abs(pixel.x+.06) < .194 limitam a repetição infinita
  • As linhas dos lábios e abaixo dos olhos são desenhadas empurrando o contorno SDF para fora
    • Um contorno normal é abs(distance_to_shape) < thickness
    • Um contorno deslocado para fora é abs(distance_to_shape - outset) < thickness
  • A linha abaixo dos olhos recebe ainda condições de posição para aparecer só em uma área específica abaixo deles

Como criar movimento com o tempo

  • A animação mais simples é feita colocando sin(time) no código
    • sin() envolve o valor crescente de time entre -1 e 1, criando uma animação repetitiva
    • O intervalo é ajustado com escala e offset, como em sin(time)*.5 + .5
  • A rotação da cabeça do Rick, a rotação da língua e a altura das sobrancelhas se movem assim
    • Para o cálculo de rotação, é adicionada a função rotateAt()
  • O piscar é implementado trocando o que é desenhado conforme o tempo
    • Se mod(time, 2.) < .09, desenha os olhos fechados
    • Caso contrário, desenha os olhos abertos e as pupilas
  • O movimento irregular das pupilas usa noise()
    • Para evitar que os olhos se movam suavemente o tempo todo, aplica-se round(time) antes de passar o valor para noise()
  • O cabelo se move com mais flexibilidade usando time domain warping
    • Em vez de distorcer o espaço, o valor do tempo é atrasado de acordo com a posição
    • Quanto mais perto da ponta do cabelo, diferente fica o atraso, então o conjunto não gira rigidamente e sim se curva

Fundo do portal e finalização

  • A versão final é concluída com a adição de um efeito de portal atrás do Rick
  • O efeito de portal é baseado em um efeito criado pela usuária do ShaderToy valena, e uma nota diz que é uma versão reduzida por FabriceNeyret2
  • O código prioriza legibilidade acima de desempenho
  • O resultado completo reúne, em um único exemplo, combinação de formas, SDF, warping, repetição, transições baseadas em tempo e movimento baseado em ruído necessários para animação 2D com shader

Fluxo de exportação de vídeo

  • O editor da página ainda não consegue exportar a animação em vídeo
  • Como solução temporária, é proposto um fluxo no macOS usando glslviewer e ffmpeg
  • Um exemplo de instalação das dependências é o seguinte
brew install glslviewer ffmpeg
  • O script de exportação cria um diretório temporário e executa glslViewer em modo headless
    • Resolução: 1920x1080
    • Sequência: de 0 a 7 segundos
    • Taxa de quadros: 60
    • Arquivo de saída: animation.mp4
  • Um exemplo de live coding local usa o seguinte comando
glslViewer shader.frag -w 575 -h 324 --noncurses -x 0 -y 0

Superamostragem e origem do texto

  • O motivo de as bordas das formas parecerem suaves é que superamostragem foi aplicada por trás
  • A superamostragem funciona chamando color_for_pixel() em 9 posições dentro de um único pixel da tela e exibindo a média
  • Ao usar #version 300 es, o modo “pro” do editor é ativado e a superamostragem automática é removida
  • O ponto de partida do texto foi o vídeo publicado há 8 meses I Made a 3D Modeler, in C, in a Week
    • Esse vídeo inclui uma animação explicando o algoritmo marching cubes
    • A percepção foi de que seria difícil fazer isso com precisão e rapidez em programas comuns de animação, então a solução foi começar a criar em código
    • Depois, como as pessoas perguntaram como a animação foi feita, isso foi organizado neste texto

1 comentários

 
GN⁺ 2025-02-08
Comentários do Hacker News
  • Se você quiser obter bordas com antialiasing suave sem uma segunda passada de supersampling, pode usar derivadas padrão no SDF
    Basicamente, a ideia é trocar a função step por aastep: https://github.com/glslify/glsl-aastep
  • Muito legal. Desenvolvedores de shaders parecem ser de outra dimensão
    É um estilo de trabalho bem diferente do desenvolvimento de web, protocolos e aplicações com que muita gente está acostumada: denso e iterativo
    É bem satisfatório mudar um float e apertar shift-enter para ver o resultado na hora
    • Fico curioso se trabalhar com abstrações como o canvas do JavaScript ou p5.js em cima dele passa essa mesma sensação
      Não sei bem se isso se refere à programação gráfica em geral ou, mais especificamente, ao trabalho com shaders de GPU
  • Um texto muito bem estruturado como introdução a GLSL
    Também existe uma playlist relacionada do Inigo Quilez no YouTube: https://www.youtube.com/watch?v=0ifChJ0nJfM&list=PL0EpikNmjs...
  • A técnica de fazer a imagem de referência piscar sobre a prévia para comparar com o original enquanto ajustava o código é exatamente a mesma usada em animação desenhada à mão
    Programação de shaders realmente é um universo à parte, e o texto é muito bom
  • Ficou muito bem organizado como introdução a GLSL
    Fico curioso sobre como seria fazer isso em Vulkan ou WebGPU/WebGL
    • É praticamente a mesma coisa. Tanto Vulkan quanto WebGL permitem usar GLSL diretamente
      No caso do Vulkan, mais precisamente, é algo como GLSL para SPIR-V
      O WebGPU no navegador tecnicamente não usa GLSL diretamente, mas implementações nativas de WebGPU podem aceitar GLSL, e também dá para converter
      Ou então é só usar WGSL, que é quase igual a GLSL, exceto pelo fato de usar uma sintaxe inspirada em Rust em vez daquela com cara de C
  • O fato de ter levado 8 meses para fazer esta animação mostra uma persistência enorme
  • Fico curioso se o processo de desenvolvimento foi uma repetição de pequenos ajustes em valores, ou qual editor foi usado
    Parece que chegar a 240 linhas com os valores decimais certos por tentativa e erro deve ter levado bastante tempo
    • Usei apenas o editor de código embutido naquela página
      Busca binária é rápida mesmo quando feita à mão
    • Ao fazer esse tipo de trabalho, normalmente você pega um slider ou algum valor de entrada e o conecta a um uniform
      O uniform é passado para o shader e pode ser atualizado sem recompilar