2 pontos por GN⁺ 2024-03-01 | 1 comentários | Compartilhar no WhatsApp
  • LUT (tabela de consulta) é uma forma de referenciar valores pré-calculados como se fosse uma tabela, usada amplamente desde colorização de imagens térmicas em preto e branco até transformação de objetos de jogo e correção de cor de vídeo
  • O exemplo em WebGL mostra o fluxo de enviar quadros de vídeo para a GPU como texturas 2D e depois aplicar tinting, LUT 1D e LUT 3D no Fragment Shader que decide a cor de cada pixel
  • Uma LUT 1D usa o valor de brilho [0.0 - 1.0] como coordenada em um array de cores para converter uma entrada em preto e branco para RGB, e a filtragem bilinear da GPU interpola naturalmente os valores intermediários mesmo com LUTs pequenas
  • Como no caso de Left 4 Dead da Valve, tinting e LUTs são usados para criar variações de carros, pele de zumbis e cores de roupas dentro de um orçamento limitado de memória de textura
  • LUTs adicionam custo de leitura extra de textura, mas são uma ferramenta prática para armazenar previamente cálculos caros como correção de gama ou levar fluxos de trabalho externos de correção de cor para gráficos em tempo real

O que LUTs fazem na programação gráfica

  • Uma lookup table (LUT) é uma forma de pré-calcular valores, armazená-los em linhas ou tabelas e consultá-los quando necessário
  • Na programação gráfica, essa estrutura simples leva a vários efeitos visuais
    • Transformar vídeo em preto e branco em colorido
    • Aplicar correção de cor e estilização
    • Criar transformações de objetos de forma eficiente em videogames
  • O exemplo usa WebGL, mas a mesma técnica também pode ser aplicada em DirectX, OpenGL, Vulkan, Unity e visualização de dados científicos
  • O vídeo de demonstração foi capturado com uma Panasonic GH6 e uma câmera térmica TESTO 890, e o processamento é feito localmente na GPU sem sair do dispositivo

Pipeline do WebGL e tinting

  • A saída da câmera térmica é um vídeo em preto e branco, e cada quadro é enviado para a GPU como uma textura 2D via WebGL
  • O Fragment Shader é o código que define a cor final de cada fragment da saída
    • No exemplo, texture2D(video, tex).rgb lê a cor do vídeo na posição atual
    • A saída final no WebGL 1.0 é gravada em gl_FragColor como RGBA
  • Tinting é a técnica de multiplicar a cor da textura por uma cor específica para aplicar uma tonalidade
    • No exemplo, usa-se videoColor * vec3(1.0, 0.5, 0.0) para aplicar laranja
    • O branco vira laranja, e o preto continua preto porque é multiplicado por 0
  • Nesse contexto, o custo da multiplicação do tinting pode ser difícil de medir
    • Buscar a textura na memória custa muito mais do que a multiplicação
    • Como várias threads executam em paralelo, enquanto a leitura de textura de um pixel espera, a multiplicação de outro pixel pode ser executada
    • Isso explica a diferença causada pelo tinting, mas não significa que toda a otimização de desempenho esteja resolvida

Como a Valve usa tinting

  • Left 4 Dead, da Valve Software, usa tinting para criar variações de carros
  • Ter uma textura separada para cada tipo e cor de carro pode estourar o orçamento de memória de textura
  • A Valve usa uma textura de máscara extra para aplicar tinting na área da pintura do carro com cores diferentes por instância
    • Uma única textura de cor é compartilhada entre 3 modelos de carro
    • Com o custo de duas texturas, dá para criar 4 modelos de carro e praticamente infinitas variações de cor
  • No Source Engine, dá para aplicar tinting não só em carros, mas essencialmente em tudo

Convertendo valores em preto e branco em cor com LUT 1D

  • Uma LUT 1D é um array numérico e, no caso de uma imagem RGB, um array unidimensional de cores
  • O valor de brilho de uma imagem em preto e branco é usado como coordenada no eixo X da LUT para buscar uma cor
    • O brilho do vídeo fica no intervalo [0.0 - 1.0]
    • 0.0 (preto) é mapeado para a cor da esquerda da LUT, e 1.0 (branco) para a cor da direita
    • Valores intermediários são mapeados para as cores correspondentes
  • Como o WebGL 1.0 não tem textura 1D, usa-se uma textura 2D com altura de 1px
    • O código do exemplo usa texture2D(lut, vec2(videoColor, 0.5))
    • A entrada é um valor 1D e a saída é RGB, então a estrutura é “1D vector in, 3D vector out”
  • A filtragem bilinear da GPU interpola automaticamente valores intermediários ao ler a textura
    • Um vídeo em preto e branco de 8 bits tem 256 níveis de brilho
    • Mesmo que a LUT 1D tenha só 32 pixels de largura, o acesso entre pixels é interpolado linearmente
    • Com informação de apenas 8 pixels, já é possível chegar a um resultado próximo de um gradiente colorido de 256 pixels de largura

Escolha de colormap e uniformidade perceptual

  • O exemplo inclui uma lista de colormaps suportados pelo matplotlib exportados como LUTs 1D
  • Se os dados não tiverem uma estrutura específica, o ideal é usar um colormap perceptualmente uniforme ou definir as cores com base nisso
    • A família viridis é um exemplo representativo
  • Um colormap perceptualmente uniforme mantém partes frias escuras e partes quentes claras mesmo quando convertido para escala de cinza
  • Colormaps como jet, que mudam principalmente o matiz e ignoram o brilho perceptual, não garantem essa propriedade
  • Também é importante escolher colormaps que permitam a interpretação correta dos dados por pessoas com deficiência na visão de cores

Desempenho de LUT 1D e aproximação polinomial

  • O custo de uma LUT 1D pequena geralmente pode ser baixo, mas há mais fatores a considerar do que no tinting
  • A principal preocupação de desempenho é a leitura de textura dependente
    • O resultado da primeira leitura de textura determina as coordenadas da segunda leitura
    • Em programação gráfica, isso pode eliminar parte dos caminhos de otimização possíveis
  • A GPU tem cache de textura, e uma LUT pequena pode caber nele, tornando o custo da leitura da LUT muito baixo
  • Para avaliar desempenho com mais precisão, são necessárias ferramentas de análise específicas da plataforma
    • O Nvidia NSight oferece análise de desempenho por estágio de shader, mas não oferece suporte a OpenGL
  • Também é possível implementar o colormap como uma aproximação polinomial, sem usar textura LUT
    • A aproximação de viridis no ShaderToy, de Matt Zucker, é escrita na forma do método de Horner
    • A cor é calculada com operações multiply-add no formato c0+t*(c1+t*(c2+t*(c3+t*(c4+t*(c5+t*c6)))))
    • Sem análise por plataforma, é difícil dizer se isso realmente será mais rápido do que uma LUT

Variações de zumbis em Left 4 Dead 2

  • Left 4 Dead 2 usa LUTs para criar variações de pele e roupas que o tinting simples não consegue representar bem
  • O tinting simples pode não oferecer variedade suficiente de luminância nas cores
  • Para pele e roupa, algumas LUTs são escolhidas aleatoriamente para gerar variações de cor
    • Os artistas escolhem color ramps adequados para incluir no jogo final
    • Uma única textura pode gerar tanto uma variação de terno claro quanto uma de terno escuro
  • A apresentação completa relacionada pode ser vista em Shading a Bigger Better Sequel, da GDC
  • “Exclusive Masking” é uma técnica de colocar duas faixas de textura dentro de um único canal
    • Texture 1 ocupa a faixa 0-128
    • Texture 2 ocupa a faixa 128-256
    • O custo é a redução da precisão de cor

Armazenando cálculos antecipadamente com LUT 1D

  • LUTs 1D também podem ser usadas na programação gráfica como cache para cálculos caros
  • Correção de gama é um exemplo clássico
    • Se não for por aproximação, é preciso usar a função pow()
    • Especialmente em GPUs antigas, pow() pode ser uma instrução cara
    • Se for usada a curva segmentada padrão sRGB, ainda há caminhos com desvio condicional
  • Ao pré-calcular a transformação de gama em uma LUT 1D, é possível pular o cálculo por pixel
    • Nesse caso, a estrutura é “1D vector in, 1D vector out”
    • Uma LUT 1D pode produzir até 4 canais de cor
  • Redshift é um exemplo de software que reduz o cansaço visual à noite aplicando um tint alaranjado ao monitor
    • Ele altera a Gamma Ramp de cada canal Red, Green e Blue do monitor
    • Os cálculos de Kelvin Warmth → RGB e a gama adicional são pré-calculados em 3 LUTs 1D
    • A implementação correspondente está no código-fonte do Redshift
  • Nessa abordagem, o remapeamento acontece no monitor, e não na placa gráfica, então não há impacto de desempenho; porém, o suporte a essa interface de hardware hoje em dia muitas vezes é instável ou quebrado
    • A pilha gráfica do Raspberry Pi perdeu esse suporte em uma atualização mais recente
    • A Microsoft alerta contra o uso de SetDeviceGammaRamp

LUT 3D: remapeando todo o espaço RGB

  • Uma LUT 3D representa todo o espaço RGB como um cubo e remapeia todas as cores possíveis
  • Ela converte um vetor RGB 3D de entrada em um vetor RGB 3D de saída, ou seja, uma estrutura “3D vector in, 3D vector out”
  • Aplicar LUTs 1D separadas a Red, Green e Blue pode alterar o balanço de cor
  • Já a LUT 3D é necessária para transformações que dependem da combinação entre os valores RGB de entrada
    • Mudança de saturação
    • Mudança de matiz
    • Ajuste de cores específicas
    • Separação de cores
  • LUTs 3D normalmente têm formato de cubo; em jogos, costumam ser armazenadas como strip ou square, e em ferramentas de edição de vídeo são usadas como arquivos .cube no formato “Iridas/Adobe”

Implementando LUT 3D em WebGL 1.0

  • O vídeo do exemplo foi gravado na Panasonic GH6 com o perfil de cor Panasonic V-Log
  • V-Log é um perfil log que preserva mais faixa dinâmica e define claramente gamut e gamma
  • Como o WebGL 1.0 não suporta texturas 3D, a leitura de textura 3D é implementada com duas leituras de textura 2D e interpolação
  • O código antigo do exemplo tinha um erro no cálculo do eixo Z do cubo que fazia as cores puxarem para o azul, e isso foi corrigido em 2019
  • Para manter compatibilidade com WebGL 1.0, OpenGLES 2 e OpenGL 2.1 sem OES_texture_3D, é importante usar a versão mais recente do código de amostragem de textura 3D

Trazendo fluxos de correção de cor com LUT 3D

  • Qualquer correção aplicada a uma LUT 3D passa a ser aplicada do mesmo jeito ao vídeo ou à cena gráfica que passar por ela
  • No exemplo, o vídeo em V-Log e a LUT foram levados ao DaVinci Resolve para processamento
    • Foi aplicada a V-Log to V-709 3D-LUT da Panasonic
    • Contraste e white point foram ajustados para que o branco ficasse em brilho total
    • A LUT corrigida foi exportada novamente
  • O vídeo real continua sendo o original; a correção de cor é aplicada em tempo real no WebGL por meio da LUT
  • A LUT enviada precisa manter o mesmo tamanho
    • A limitação do exemplo é 1024px × 32px
    • É preciso preservar o formato de cubo 32³px

Como a indústria de games usa LUT 3D

  • Estilizar as cores de um jogo em ferramentas externas com LUT 3D é um fluxo de trabalho padrão bem conhecido na indústria de videogames
  • O processo geral costuma ser este
    • Tirar uma captura de tela da cena a ser corrigida
    • Abrir a captura e uma LUT 3D inicializada em uma ferramenta como o Photoshop
    • Aplicar a mesma correção de cor tanto na captura quanto na LUT
    • Recortar a parte da LUT 3D e exportá-la
  • Left 4 Dead também usava essa mesma abordagem
  • As ferramentas de correção de cor do Photoshop podem ser usadas livremente, mas filtros que mudam a relação entre vários pixels não podem
    • Filtros de convolução como blur, sharpen e emboss podem borrar a cor remapeada e gerar resultados inesperados

Assando uma correção de cor complexa em uma única LUT

  • Ferramentas de correção de cor como o DaVinci Resolve conseguem criar transformações de cor e color grades complexos e exportá-los como LUT 3D
  • O exemplo aplica uma LUT Kodak 2383 com o objetivo de obter um visual de impressão em filme
    • Converte vídeo com gamma V-Log em Cineon Film Log
    • Converte o espaço de cor para Rec.709
    • Aplica emulação de filme e depois converte a gamma de volta para Rec.709
    • Por fim, ajusta o white point
  • Em vez de implementar manualmente cada transformação detalhada, tudo pode ser assado em uma única LUT e levado para a aplicação gráfica
  • O vídeo do exemplo está em formato altamente comprimido de 8 bits por compatibilidade com o artigo, mas esse tipo de trabalho normalmente é feito com footage de 10 bits
  • O tamanho das LUTs usadas costuma ser menor do que parece
    • A 3D-LUT oficial V-Log to V-709 da Panasonic tem tamanho 33³px
    • As LUTs de monitoramento interno das câmeras Panasonic usam 17³px
    • Até câmeras de cinema da Panasonic na faixa de dezenas de milhares de dólares usam 17³px

Exemplos de LUTs fora da computação gráfica

  • LUTs também são usadas fora da programação gráfica
  • Elas apareceram até em um caso de port de Tomb Raider para Game Boy Advance
  • O port do engine open source OpenLara processa muita coisa em software porque o Game Boy Advance não tem recursos 3D
  • Um desses casos é uma LUT para implementar divisão inteira
  • Essa LUT foi colocada no início do espaço de endereços da ROM para evitar uma instrução de load

1 comentários

 
GN⁺ 2024-03-01
Comentários do Hacker News
  • Acho que eu nem teria conseguido implementar esse efeito sem uma lookup table: https://twitter.com/zeta0134/status/1756988843851383181
    O ponto principal é que são usadas duas lookup tables. A tabela grande armazena informações de iluminação circular dentro de um raio configurável ao redor do jogador, com uma tabela inteira para cada raio. A segunda tabela contém uma ordem pseudoaleatória das linhas do fundo. Cada vez que a rotina de iluminação da tocha é chamada, só há tempo para atualizar 1/20 da tela, mas, ao randomizar um pouco a ordem, as bordas parecem mais suaves e dá para esconder o raster scan que normalmente ficaria visível. Essa ordem é uma grab bag para que nenhuma linha fique sem atualização; o cálculo em tempo real é lento demais, então usa-se uma tabela

    • Parece bem legal e os gráficos são muito melhores do que qualquer jogo de NES de que me lembro, quase no nível do SNES. Parece estar rodando em um emulador; fico curioso se esse emulador limita o desempenho ao nível do NES
    • Parece um jogo realmente incrível. Fico pensando se foi inspirado em Crypt of the Necrodancer. É um dos meus jogos favoritos. Eu já tinha pensado até que ponto o NES ou outros consoles antigos dariam conta se uma mecânica dessas tivesse sido inventada na época, e isso parece mostrar que era possível
    • A atualização de 1/20 funciona bem porque, quando a lâmpada se move, dá a ilusão de que os tiles vão sendo revelados naturalmente. É legal que dê para se virar com esse tipo de efeito
    • Sempre é interessante ver jogos para o mesmo console ficando cada vez mais bonitos à medida que as pessoas aprendem a programar melhor. Por exemplo, Mario 1 e 3, ou os dois Zelda do N64
    • O termo para pesquisar sobre a parte de “manter uma ordem aleatória como grab bag para que nenhuma linha fique sem atualização” provavelmente é quasirandom
  • O primeiro efeito com lookup table que realmente me impressionou foi o uso para criar um túnel texturizado
    Você tem uma lookup table que, para cada pixel da tela, informa o ângulo e a distância em relação ao centro da tela, e usa isso para escolher qual texel colocar em cada posição de pixel. Parece que você está se movendo dentro de um túnel de geometria 3D, mas o custo é muito baixo, então é possível até no pico: https://www.lexaloffle.com/bbs/?pid=63818
    No começo achei que Stardust tivesse usado esse efeito, mas acabei de procurar e, na verdade, ele só reproduzia uma animação de 6 quadros repetida no fundo: https://codetapper.com/amiga/sprite-tricks/stardust/

    • Se você encarar uma paleta de cores como uma lookup table, a ciclagem de paleta também era muito comum e parece bem relacionada
    • Uma aplicação sofisticada desse princípio é o parallax occlusion mapping (POM). Dá até para fazer auto-sombreamento
      https://web.engr.oregonstate.edu/~mjb/cs557/Projects/Papers/Parallax_Occlusion_Mapping.pdf
    • Se quiser ver como funciona, há um pequeno botão Code ▽ abaixo da demo
    • Observar a simetria em certos casos pode ajudar a encontrar uma solução prática
  • Há um vídeo sobre como Wind Waker usa várias LUTs para criar seu visual característico. BoTW e ToTK também usam a mesma técnica
    https://www.youtube.com/watch?v=mnxs6CR6Zrk

  • Sou o autor do artigo. Posso responder se houver perguntas

    • Os exemplos de uso de LUT são ótimos, e é bom ver um artigo que explica isso de forma clara. Também gostei do fato de usar WebGL e permitir enviar os próprios dados
      Mas fiquei um pouco surpreso com a forma como o artigo apresentou a correção de cores usando LUT. No texto, parece uma solução de nicho bacana usada por L4D2, mas na prática isso é padrão da indústria há muito tempo e foi usado em todos os jogos em que trabalhei. Desde jogos AAA como NFS (2015) até indies grandes como Lost in Random
    • Gostei do artigo e queria complementar um pouco a última seção. O Game Boy Advance tinha algum suporte a divisão de inteiros, mas não era acelerado por hardware [1]. O modelo com LUT provavelmente foi escolhido por motivos de desempenho, e essa decisão parece fazer sentido
      [1] https://www.copetti.org/writings/consoles/game-boy-advance/#operating-system
    • Por causa de um typo, fiquei um bom tempo confuso achando que existia outra biblioteca de plotagem
      “Here is every single colormap that matlibplot supports,...”
    • A quantidade de GIFs animados é irritante e distrai demais. Os memes sem graça de boomer dos anos 2010 também são dispensáveis. Fica difícil ler o artigo com GIFs coloridos pulando o tempo todo
  • Isso também é surpreendentemente útil até na área tediosa de processos de negócio
    Muitas vezes dá para simplificar uma base de código cheia de condicionais transformando-a em uma lookup table limpa. Acho que nem sempre as pessoas lembram disso porque até algo aparentemente simples pode virar uma lookup table de 50 mil linhas. Para o usuário final pode parecer muito, mas para o computador não faz muita diferença. Além disso, lookup tables são repetitivas, então geralmente são fáceis de manter; e, quando não são, ainda assim podem valer a pena. Também podem ser configuradas pelo usuário final sem mudar código. No geral, é um conceito especialmente útil para vários cenários que sempre aparecem ao programar lógica de negócios

    • É assim que você acaba criando uma DSL
    • Lookup table com buffer duplo, essa é nova!
  • Jogos retrô usavam tabelas em enorme quantidade. Na época, a memória era muito rápida e os processadores eram lentos, então fazia sentido empurrar o máximo possível de cálculos para tabelas. Quanto mais esperto você fosse nisso, mais vistosos os jogos podiam ser

    • Nem sempre; acho que depende de qual plataforma retrô estamos falando. O Kaze Emanuar, no YouTube, faz bastante desenvolvimento para N64, e mais ou menos metade das discussões de otimização é sobre como o barramento de memória afeta todo tipo de otimização. Em Mario 64, a tabela de consulta existente para a função seno foi trocada por uma aproximação, porque a aproximação era mais rápida e precisa o bastante. Mais exatamente, eram duas aproximações para finalidades diferentes
      Gosto desse canal. Ele mexeu em todo o código de Mario 64 para fazer o mod dele rodar mais rápido, chegando a 60 FPS estáveis [0]
      [0] https://www.youtube.com/watch?v=t_rzYnXEQlE
    • Lembro da época em que a precisão de rotação era limitada a unidades de 360/256 graus para caber em um único byte, e esse valor era usado como índice em uma tabela de consulta trigonométrica :)
  • Se você lida muito com LUTs, estou desenvolvendo um app para Mac que trata de ciência de cor avançada e trabalhos de conversão: https://videovillage.com/lattice

  • Um tempo atrás, explorei um pouco a cena de homebrew para NES e vi no jogo Bobl alguns efeitos de física impressionantes, muito além do que o sistema conseguiria calcular. Descobri que eram tabelas de consulta, e percebi como uma ferramenta simples pode fazer algo parecer um processo muito complexo
    https://morphcatgames.itch.io/bobl

    • Não nego que o efeito seja impressionante, mas, se você está falando das ondulações na superfície ou de ondas capilares, fico me perguntando se seria realmente difícil para o NES calcular em tempo real um autômato celular unidimensional
  • A Podcast Of Unnecessary Detail lançou um episódio sobre o port de Doom para SNES e como LUTs foram usadas para funções trigonométricas. O contexto é que o SNES não tinha processador gráfico
    https://festivalofthespokennerd.com/podcast/series-3-episode-4/
    https://github.com/RandalLinden/DOOM-FX

    • Dizer que “o SNES não tinha processador gráfico” está completamente errado. Até o NES tinha um. Além da CPU do SNES, um clone do Motorola 65c816, a PPU (picture processing unit) era um chip customizado que fornecia vários layouts para emitir fundos baseados em tiles e sprites enquanto a TV CRT desenhava fisicamente cada scanline
      Ela também permitia rotação e escalonamento de fundos. É o caso do Mode 7, pense em Mario Kart, e também havia suporte a transparência por hardware, vista em vários jogos. Além disso, no caso de Doom e Starfox, o cartucho trazia o chip Super FX para cuidar dos cálculos 3D. Portanto, nesses títulos específicos, na prática você estava lidando com dois processadores gráficos
      Há uma série do Retro Game Mechanics Explained que trata do hardware do SNES de forma excelente e dolorosamente detalhada:
      https://www.youtube.com/watch?v=57ibhDU2SAI&list=PLHQ0utQyFw5KCcj1ljIhExH_lvGwfn6GV&index=1
  • Usos de LUT que me vêm à cabeça e que eu já implementei foram espalhamento atmosférico, colorização de sprites, mira de visão noturna, mira FLIR, efeito de “feed de vídeo” em preto e branco, efeito de glitch, sombreamento de heightmap, coeficientes de pontos alfa da pluma de exaustão de naves espaciais, heatmap de permanência do mouse de visitantes de um site, efeito cristalino e, por fim, colorização em pós-processamento no espaço de cores bruto
    Uma LUT é uma visualização de uma matriz de valores que você já conhece, e é surpreendentemente útil

    • “Colorização de sprites”? As crianças de hoje se esqueceram totalmente dos sprites baseados em paleta! De onde vocês acham que veio o termo “palette swap”? ;)
      http://www.effectgames.com/demos/canvascycle/
      O Xbox original não lidava muito bem com isso, e o Photoshop também não dava suporte, então a popularidade caiu. Mas o hardware moderno consegue lidar tranquilamente com palette swaps feitos no AESprite