Como renderizar texto nítido na GPU
(osor.io)- A abordagem tradicional de Multi-Channel SDF ainda tinha limitações com traços finos, atlases grandes, redução/ampliação e movimento suave, então foi reimplementada com rasterização em tempo de execução na GPU
- O novo pipeline envia para a GPU apenas os dados de curvas Bézier dos glifos visíveis no momento, rasteriza no atlas em tempo de execução e depois faz a amostragem na tela
- Se o glifo permanece na tela, o cache do atlas é mantido e as amostras são acumuladas para aumentar a qualidade, começando com 8 samples-per-pixel no primeiro frame e refinando até 512 samples no máximo
- A chave de cache inclui font, glyph_index, tamanho em pixels e offset subpixel, usando ponto fixo de 8 bits para agrupar posições e tamanhos próximos no mesmo valor e aumentar a reutilização
- Ao modelar a estrutura RGB de subpixels de cada monitor como áreas de amostragem, é possível reduzir o color fringing até em arranjos não padronizados como o OLED G9, e uma demo de texto em tela cheia 4K mostrou custo de pico de cerca de 0,1 ms em uma Radeon 9070
Por que escolher rasterização vetorial em tempo de execução em vez de SDF
- A implementação anterior usava Multi-Channel Signed Distance Fields e, no geral, funcionava bem, mas no uso real ainda havia algumas limitações
- Qualidade: em fontes com traços finos ou muitos detalhes, características se perdiam ou surgiam artefatos, e às vezes era necessário um SDF de resolução mais alta
- Tamanho do atlas: como o SDF é gerado offline e armazenado em um atlas, fontes japonesas e chinesas com muitos glifos são difíceis de pré-cozinhar em um único atlas
- Flexibilidade: era difícil aplicar novas ideias, como problemas de redução/ampliação ou antialiasing subpixel, e também não se encaixava bem em imagens vetoriais geradas e editáveis em tempo de execução
- Simplicidade: a etapa de converter as curvas originais dos glifos em uma textura intermediária aumentava a complexidade do sistema
- Até uma fonte latina como Miama exigia um atlas
4096×1152usando áreas de64×64por glifo, e usar várias fontes em tempo de execução aumentava o custo de memória e de largura de banda de streaming - O objetivo da nova abordagem é usar de forma mais direta as curvas Bézier criadas pelo autor do glifo, reduzindo as etapas de transformação entre os dados originais e o pixel final
Pipeline completo
- A ideia central é, em vez de pré-cozinhar todos os glifos offline, enviar para a GPU apenas as curvas dos glifos visíveis no momento e rasterizá-las quando necessário
- O fluxo de processamento é simples
- Carregar do font os dados de curva dos glifos
- A GPU rasteriza os glifos no atlas em tempo de execução
- Na saída para a tela, esse atlas é amostrado
- Se o mesmo glifo continuar sendo usado nos frames seguintes, ele permanece no atlas e as amostras são acumuladas para refinar até um antialiasing subpixel de alta qualidade
- Como a representação vetorial é renderizada diretamente, fica fácil lidar com mudanças de resolução, e é possível calcular não só a cobertura total do pixel, mas também a cobertura de cada elemento subpixel
Processamento das curvas dos glifos
- Para carregar fontes, usa-se FreeType como camada intermediária da ferramenta offline para ler os formatos suportados, percorrer as curvas de cada glifo e salvá-las em um formato próprio de asset
- As curvas dos glifos podem incluir linhas, Bézier quadrática e Bézier cúbica, mas para simplificar o shader todas são convertidas para Bézier quadrática
- Linhas viram Bézier quadrática adicionando um ponto de controle no meio entre os dois pontos
- Bézier cúbica é dividida em duas Bézier quadráticas, o que é uma conversão com perda por redução de grau
- Esse método de dividir uma Bézier cúbica em duas Bézier quadráticas funcionou bem na maioria das fontes testadas, mas métodos mais sofisticados também podem reduzir ainda mais o erro
- Se for necessária uma conversão de qualidade mais alta, é possível usar uma ferramenta offline para converter para um formato com apenas Bézier quadrática, como TrueType
.ttf, evitando essa conversão - No gráfico do Desmos é possível comparar a forma da Bézier cúbica de entrada com as duas Bézier quadráticas resultantes
Cálculo de cobertura e otimização de acesso às curvas
- A cobertura é calculada por pixel lançando um raio horizontal da esquerda para a direita e verificando as interseções com as curvas para acumular o winding number
- A base matemática e a implementação podem ser consultadas em GPU Font Rendering, de GreenLightning e no vídeo Rendering Text, de Sebastian Lague
- Imprecisões no cálculo de interseção podem ocorrer em amostras de certas alturas, mas ao acumular centenas de amostras um ou dois erros quase desaparecem após a média
- Ao acumular no máximo 512 amostras, se uma amostra estiver errada, o resultado vira
1/512=0.00195em vez de0, ou511/512=0.99804em vez de1 - Também é possível definir limiares para clamp quando a cobertura estiver próxima dos extremos
- Ao acumular no máximo 512 amostras, se uma amostra estiver errada, o resultado vira
- Para a distribuição das posições de amostragem, usa-se a $R_2$ sequence de Martin Roberts, e a distribuição ao longo do tempo pode ser vista neste exemplo do Shadertoy
- Para reduzir o custo de acesso às curvas, o glifo é dividido em várias faixas horizontais, e apenas as curvas que tocam cada faixa são armazenadas em um bitset
- Como só são rastreados raios horizontais, isso reduz bastante o conjunto de curvas que cada pixel precisa verificar
- Se waves acessarem o mesmo intervalo de faixas, a iteração e leitura das curvas podem ser escalarizadas
- Ao rasterizar no atlas com um compute shader, as threads são empacotadas horizontalmente em row-major para minimizar o intervalo de faixas tocado por cada wave
Empacotamento do atlas e chave de cache
- No começo, a rasterização era feita diretamente na tela, mas o custo de calcular antialiasing de alta qualidade em todo frame era alto
- Como a maior parte do texto permanece no mesmo tamanho e posição por vários frames, e o mesmo glifo se repete no mesmo tamanho, atlas e acumulação temporal são uma boa combinação
- O atlas aloca espaço e inicia a rasterização quando o glifo necessário não existe; se ele já existir, o resultado anterior é reutilizado tal como está
- Durante o frame, os glifos do atlas são verificados para decidir se devem ser mantidos, receber mais amostragem ou liberar espaço por falta de uso
- A chave do atlas inclui os seguintes elementos
fontglyph_indexquantized_size_in_pixels_x,quantized_size_in_pixels_yquantized_subpixel_offset_x,quantized_subpixel_offset_y
- O offset subpixel corresponde a
frac(pixel_position)e é necessário para gerar o resultado de antialiasing adequado à posição quando o glifo não está exatamente alinhado à grade de pixels ou quando há rolagem suave - Se valores de ponto flutuante forem usados diretamente como chave, valores que deveriam ser matematicamente iguais podem diferir no nível de bits; por isso, posições e tamanhos próximos são agrupados no mesmo valor com ponto fixo de 8 bits na parte fracionária
- Em um editor de texto com muito texto estático usando fonte monoespaçada, alinhar o espaçamento entre caracteres e a posição das linhas aos limites de pixel pode aumentar a taxa de acerto do cache de atlas do mesmo glifo
Posicionamento no atlas com base em Z-Order
- A colocação de glifos em tempo de execução usa Z-Order Packing e um bitset de células livres
- O Z-Order baseado em código Morton permite tratar células 2D como se fossem um longo arranjo 1D, e alocar células contíguas em quantidade de potência de 2 gera uma região quadrada no atlas 2D
- A célula básica é de
16×16texels, e o tamanho do glifo é arredondado para a próxima potência de 2- Por exemplo, um glifo
25×29aloca um bloco32×32 - Nesse caso, são necessárias 4 células
16×16, então basta encontrar 4 bits contíguos alinhados
- Por exemplo, um glifo
- Como muitos glifos longos e finos do alfabeto latino são verticais, usar Z-Order transposto permite que glifos como
l,j,i,1usem apenas metade do espaço - Já em casos como o árabe, em que glifos longos e finos tendem a ser horizontais, o Z-Order padrão é mais adequado
Como a acumulação temporal aumenta a qualidade
- Enquanto o glifo permanece no atlas, mais amostras podem ser adicionadas a cada frame para refinar o resultado
- O cronograma básico usa 8 samples-per-pixel no frame em que o glifo aparece pela primeira vez, depois 4 no frame seguinte, 2 no próximo e então 1 adicional por frame até acumular 512 no total
- A qualidade inicial mais alta é importante porque glifos que se movem suavemente ou mudam de tamanho são, na prática, reinicializados a cada frame
- Qualidade e desempenho podem ser ajustados de várias formas
- Número de amostras/raios adicionados por frame
- Se haverá aumento de amostras nos primeiros frames do glifo
- Limite total de amostras por frame
- Time-slicing para atualizar glifos existentes a cada alguns frames, em vez de em todo frame
- Formas de limitar o custo de verificação de interseções conforme a quantidade de curvas do glifo
- Nesta implementação, desempenho não foi um grande problema: o texto em tela cheia da introdução mostrou pico de cerca de 0,1 ms em 4K numa Radeon 9070, e quando os glifos atingem a quantidade máxima de amostras o custo rapidamente se aproxima de zero
Antialiasing subpixel e color fringing
- O antialiasing subpixel renderiza tratando separadamente os elementos vermelho, verde e azul dentro de cada pixel do monitor como áreas de amostragem
- Na estrutura tradicional de faixas verticais RGB dos LCDs, a resolução horizontal efetiva praticamente triplica, então em 4K
3840×2160pode ser tratado como algo como3840×6480 - O problema é que monitores como o OLED G9 têm uma estrutura de subpixels não padronizada, diferente das faixas verticais RGB tradicionais
- Se assumir a estrutura retangular vertical RGB padrão, aparece color fringing verde na parte superior e magenta na inferior
- Se as posições de amostragem forem ajustadas à estrutura real de subpixels do monitor, quase não há color fringing e o resultado fica suave
- O Subpixel Zoo mostra exemplos de várias estruturas de subpixels, e a estrutura red-white-blue-green do LG WOLED é um exemplo de ordem diferente da padrão
- Resultados mais precisos foram obtidos tratando os elementos subpixel como maiores que seu tamanho físico real e com sobreposição entre si
- A luz dos subpixels naturalmente se mistura e se espalha um pouco, então a área de amostragem se comporta como se fosse maior que o subpixel físico
- A área de amostragem também deve se espalhar para fora do pixel e se misturar com a luz dos subpixels dos pixels vizinhos
- Easy Scalable Text Rendering, de Evan Wallace defende a necessidade de um blur horizontal após o antialiasing subpixel, o que na prática produz o mesmo efeito de tratar os elementos subpixel como maiores e sobrepostos
O software precisa conhecer as informações de subpixels do display
- Se fosse possível acessar a estrutura arbitrária de subpixels do monitor, seria possível melhorar o antialiasing subpixel em geral e a qualidade da renderização de texto
- Se esse tipo de informação fosse fornecido por protocolos de display comuns, até monitores com arranjo padrão poderiam receber renderização mais refinada e específica para o hardware
- Fabricantes de displays não precisariam evitar experimentar estruturas de subpixels melhores por causa de problemas de renderização de texto
- A Samsung alterou a estrutura de subpixels do G8 para o G9 no QD-OLED para reduzir esse tipo de problema, e color fringing é uma crítica comum tanto em LG WOLED quanto em Samsung QD-OLED
- Há espaço para tratar esse problema com correção por software, em vez de depender de troca de hardware
O valor prático da renderização de glifos em tempo real
- Uma boa UI e boa qualidade de texto podem elevar a percepção geral de qualidade de um produto
- Em jogos, elementos que atraem muito o olhar do usuário — como caixas de texto, menus, títulos e notificações — aparecem com frequência, e a queda na qualidade do texto pode afetar a experiência tanto quanto uma cena 3D mal renderizada
- A série Persona, Metaphor: ReFantazio e Nier: Automata são bons exemplos de UI e apresentação de texto
- Tentativas de melhorar a qualidade da renderização de glifos em tempo real têm valor prático tanto para UI quanto para rendering de jogos
1 comentários
Opiniões do Hacker News
O que aconteceu com o ponto do j em itálico no primeiro vídeo?
A renderização de fontes por subpixel é importante para a legibilidade, mas, como o texto aponta, é uma pena que não seja possível obter a especificação do arranjo dos pixels nos padrões de display existentes
O mundo foi migrando cada vez mais para displays de nível Retina, onde quase não há motivo para renderização por subpixel
Há muitos incômodos: screenshots ficam vinculadas a um arranjo específico de subpixels, e ampliar/reduzir bitmaps também é difícil
Foi uma inovação temporária da era dos LCDs entre o CRT e o Retina, e hoje é mais uma tecnologia voltada para trás. Há bons motivos para a Apple tê-la removido do macOS alguns anos atrás
Fico curioso se os fabricantes de displays é que não implementam. De qualquer forma, para os modelos de display mais comuns, é uma informação que poderia ser facilmente inferida e armazenada em um banco de dados de informações de hardware
O texto é excelente e também traz um link para um “subpixel zoo”, com vários exemplos: https://geometrian.com/resources/subpixelzoo/
Mesmo nos casos inevitáveis em que o monitor informa o arranjo errado, esse tipo de abordagem ainda seria necessário
Fontes bitmap sem antialiasing ou fontes vetoriais com hinting já oferecem ótima legibilidade
Ela só se torna importante em idiomas com caracteres de detalhes muito complexos, como chinês ou japonês
O GTK4 abandonou a renderização por subpixel RGB ao mover a renderização para a GPU
Ouvi dizer que decisões centradas na GPU tornaram difícil continuar com a renderização por subpixel RGB, mas o texto mostra que é possível
Então talvez o motivo do GTK estivesse em outro lugar, ou a solução apresentada tenha desvantagens, ou não se integre bem à stack existente
Se você tem interesse em como implementar SDF e MSDF em WebGL / WebGPU, veja o tutorial que escrevi: https://infinitecanvas.cc/guide/lesson-015#msdf
Já tentei portar exemplos em JavaScript para Rust; não dá para simplesmente copiar e colar, e a API é parecida o suficiente para que o port seja fácil, o que é ideal para aprender
Também acaba sendo um bom motivo para se familiarizar com a documentação do WGPU
Gosto de criar tutoriais relacionados a GPU e queria estruturá-los desse jeito; fico curioso se é um template existente ou parte de algum curso
A biblioteca Slug é um middleware comercial que implementa esse tipo de rasterizador de glifos na GPU
[1]: https://sluglibrary.com/
Seria divertido criar uma versão open source em wgpu usando parte do parsing e do layout de fontes do cosmic-text, mas não teria graça nenhuma se, no fim, a Slug processasse você
A GPU parece ter uma capacidade praticamente infinita de desenhar vértices/pixels; ainda não entendo por que é preciso renderizar texto offline, armazená-lo em um atlas e até usar truques como SDF
O texto também diz que escreve as curvas dos glifos no atlas, mas fico me perguntando por que o shader não pode renderizar o texto diretamente
Com certeza deve haver uma forma de transformar Béziers em malhas de triângulos. Estou prestes a começar um renderizador de texto em GPU para um app de CAD, então espero descobrir logo o motivo
A GPU é rápida, mas não infinitamente rápida, e é muito boa em amostrar texturas pré-renderizadas
Não é apenas uma questão de velocidade, mas também de consumo de energia. Se você já atingiu a taxa de atualização do monitor, melhorar ainda mais a performance não aumenta a responsividade, mas pode aumentar a duração da bateria
Em renderização, não existe “rápido o suficiente”; ficar mais rápido sempre traz algum benefício
Todas as arquiteturas modernas de GPU são ruins para processar geometria de alta densidade. Nesses casos, simplesmente empurrar triângulos para a GPU é muito ineficiente em comparação com um atlas ou outras técnicas
A maioria das GPUs despacha pixel shaders em grupos de 4. Se todos os triângulos tiverem tamanho de 1 pixel, 3 threads de shader não contribuirão para a saída visual
Isso é chamado de quad overdraw. Além disso, você também gasta muito tempo no processamento de vértices sem um motivo real
É possível, mas você acaba abrindo mão de parte do orçamento de frame e aumentando o consumo de energia sem muito ganho
O autor usa atlas porque faz supersampling de curvas Bézier com até 512 amostras por pixel, o que é muito caro
Em vez disso, calcular a integral da interseção entre a área da curva Bézier e a área do subpixel poderia ser muito mais rápido e, rodando em tempo real sem atlas, provavelmente seria mais preciso do que supersampling
E quase sempre você vai querer usar esse tempo em outra coisa
Quanto mais tempo de GPU for necessário, mais rápido também terá de ser o hardware mínimo exigido. Texto é bonito e importante, mas talvez não seja importante a ponto de perder usuários ou clientes
Parece pior do que apenas dizer que “os novos OLEDs são bonitos, mas têm problemas de franja de cor por causa da estrutura de subpixels não padrão”
Pelo que entendi, não é simplesmente algo não padrão: nos OLEDs há várias disposições de subpixels incompatíveis entre si
Por isso o FreeType não implementou renderização subpixel para OLED, e isso vira um motivo para evitar OLED se você precisa trabalhar com texto
Também não é um problema só do FreeType; toolkits de GUI como Qt e GTK também teriam que se ajustar. Não sei bem se há progresso na solução
Seria bom poder acessar a estrutura de subpixels de um monitor arbitrário, e talvez esse tipo de informação devesse ser transmitido via EDID
Por exemplo, meu notebook usa uma disposição BGR na direção vertical, e o FreeType e o KDE dão bom suporte a ela
Acho que essas disposições estranhas muitas vezes surgem porque, em displays HDR, é preciso usar tamanhos diferentes para cada cor para evitar que certas cores, especialmente o azul, queimem rápido demais
É um trabalho muito impressionante
Para quem não é familiarizado com essa área: a Valve criou a renderização de texto SDF para jogos e publicou em 2007 um artigo seminal sobre o tema
É uma técnica que ainda hoje é muito usada em videogames, quase sem modificações
Em 2012, Behdad Esfahbod criou o Glyphy, uma implementação de SDF que roda na GPU com OpenGL ES, e ela foi bastante reconhecida pelo desempenho e por novos recursos como transformação rápida de texto, mas não chegou a ser amplamente usada
Sistemas operacionais modernos e navegadores web preferem depender da rasterização TrueType no estilo dos anos 1990 em vez dessas técnicas
É uma abordagem leve e eficaz, mas, como o texto mostra, não permite alinhamento subpixel nem disposições arbitrárias de subpixels; zoom tem um custo alto de desempenho; e transformações complexas como inclinação, rotação e transformações 3D não podem ser feitas dentro do mecanismo de renderização de texto
Se você precisa de texto rotacionado ou transformado, precisa reamostrar um bitmap, o que destrói todos os pequenos detalhes que dão legibilidade e não fica bonito
O motivo do progresso lento talvez seja que o volume de trabalho e o risco são grandes demais em relação ao ganho. Imaginar reescrever um motor moderno de navegador web com renderização de texto acelerada por GPU não é pouca coisa
A renderização de glifos é só uma parte; a quebra de linhas é outro problema. Pode ser lenta porque exige muita comunicação entre CPU e GPU, e a integração profunda entre software e GPU também é difícil
Esse método tem desempenho muito melhor do que tentar encaixar à força no pipeline de renderização 3D do hardware, como no SDF
O Safari provavelmente também é, mas não posso afirmar por experiência direta
A ideia de que o estado da arte ou as implementações entregues aos usuários não evoluíram está errada
SDF codifica a distância local (Distance) de um determinado pixel até a borda da letra como um campo (Field), isto é, um array bidimensional de dados, e indica com um bit de sinal (Sign) se essa distância fica dentro ou fora da letra
Cada letra tem um pequeno mapa de dados, que é empacotado junto em um formato de arquivo de imagem amigável à GPU, e um shader de renderização SDF o usa junto com um arquivo de descrição que indica onde encontrar a subimagem de cada letra
Esse tipo de definição de letra é muito robusto à interpolação linear entre os valores do campo, permitindo ampliação quase perfeita mesmo com mapas de resolução relativamente baixa. GPUs também são boas em interpolar valores de pixels do mapa
Mas o ponto importante é que esses mapas precisam ser pré-processados, durante o desenvolvimento, para todos os caracteres que você pretende renderizar a partir do sistema de fontes existente. Isso é necessário para cada caractere suportado pela fonte
É muito menos dado do que renderizar todos os caracteres como uma fonte bitmap de alta resolução, mas é muito mais do que as próprias definições de contorno da fonte
Sistemas que tentam dar suporte a todos os textos possíveis do mundo, como sistemas operacionais ou navegadores, não conseguem usar SDF como sistema de renderização de texto. Seriam necessários mapas SDF para todo o conjunto de caracteres Unicode, o que ficaria grande demais
Jogos se encaixam bem porque, em geral, não precisam ter localização perfeitamente completa nem exibir texto completamente arbitrário
SDF originalmente também não consegue dar suporte a emojis, porque só codifica a distância até a borda do glifo e não contém informações de cor dentro do glifo
Existe uma versão aprimorada chamada Multichannel SDF, que dá suporte a várias cores, mas ainda há um limite para o número total de cores
Na prática, se você observar de perto um jogo que usa SDF para o texto dentro do jogo e também tem um sistema de chat em que uma comunidade global interage, é bem provável que a renderização de texto do jogo e a do sistema de chat sejam diferentes
Segundo https://keithclark.co.uk/articles/gpu-text-rendering-in-webk... (2014), “quando um elemento é promovido para a GPU nas versões atuais do Chrome, Safari ou Opera, ele perde o antialiasing subpixel e o texto é renderizado em escala de cinza”
Nesse caso, fico curioso sobre o que está faltando. Do jeito que essa frase está escrita, parece que pelo menos parte da etapa que vai de uma string UTF-8 a um bitmap poderia ser feita na GPU
É um trabalho impressionante
Mas, pessoalmente, acho que antialiasing subpixel não faz muito sentido. Era um hack razoável nos anos 2000, quando se usavam monitores de 72 dpi, mas em telas Retina modernas é difícil de perceber, e você ganha várias desvantagens por uma melhoria muito pequena
Só funciona sobre fundos opacos, não permite aplicar efeitos como redimensionamento, espelhamento ou blur ao resultado rasterizado, e capturas de tela ficam piores quando vistas em outro display
Segundo a pesquisa de hardware do Firefox [1], 16% dos usuários usam displays com resolução 1366x768
Não é só uma questão de hardware antigo: monitores e notebooks de 96 dpi ainda são fabricados hoje
[1]: https://data.firefox.com/dashboard/hardware
Os outros argumentos não são muito importantes em comparação com os melhores resultados que a renderização subpixel oferece onde ela é aplicável
O ponto importante é que SDF calcula a distância em pixels até a borda mais próxima, enquanto renderizadores de fontes tradicionais calculam a cobertura do pixel
A cobertura do pixel é o ideal. Em fontes pequenas, o SDF pode ficar ruim nas partes em que as bordas se encontram
Talvez isso seja menos problemático em displays de alto PPI. Implementei meu próprio renderizador SDF e ele ficou pior que o FreeType
Mas está correta a observação de que campos de distância causam problemas em interseções, ou, de modo geral, em cantos agudos
Isso pode ser mitigado até certo ponto usando múltiplos campos de distância e renderizando sua interseção. Um exemplo é https://github.com/Chlumsky/msdfgen