Existe uma maneira de usar um número arbitrário de luzes em um shader de fragmento?

19

Existe uma maneira de passar um número arbitrário de locais de luz (e cores) para o shader de fragmento e fazer um loop sobre eles no shader?

Caso contrário, como as luzes múltiplas devem ser simuladas? Por exemplo, no que diz respeito à iluminação direcional difusa, você não pode simplesmente passar uma soma dos pesos de luz do shader.

NotRoyal
fonte
Não trabalho com WebGL, mas no OpenGL, você tem no máximo 8 fontes de luz. Na minha opinião, se você deseja passar mais do que isso, deve usar, por exemplo, variáveis ​​uniformes.
Zacharmarz 09/11
O método antigo era sempre passar em todas as luzes, luzes não utilizadas eram ajustadas para 0 de luminância e, portanto, não afetavam a cena. Provavelmente não muito usado mais ;-)
Patrick Hughes
7
Quando você pesquisar no Google assim, não use o termo 'WebGL' - a tecnologia é muito nova para as pessoas pensarem em abordar esses problemas. Tomemos esta pesquisa, por exemplo, 'Estou com sorte' teria funcionado. Lembre-se de que um problema do WebGL deve se traduzir perfeitamente no mesmo problema do OpenGL.
Jonathan Dickinson
Para mais de 8 luzes na renderização para frente, geralmente uso um sombreador de várias passagens e dou a cada passagem um grupo diferente de 8 luzes para processar, usando a mistura aditiva.
91811 ChrisC

Respostas:

29

Geralmente existem dois métodos para lidar com isso. Atualmente, eles são chamados de renderização direta e renderização diferida. Há uma variação desses dois que discutirei abaixo.

Encaminhar renderização

Renderize cada objeto uma vez para cada luz que o afetar. Isso inclui a luz ambiente. Você usa um modo de mesclagem aditiva ( glBlendFunc(GL_ONE, GL_ONE)), para que as contribuições de cada luz sejam adicionadas uma à outra. Uma vez que a contribuição de diferentes luzes é aditiva, o buffer de quadros acaba obtendo o valor

Você pode obter o HDR renderizando em um buffer de moldura de ponto flutuante. Em seguida, você passa pela cena final para reduzir os valores de iluminação HDR para um intervalo visível; também seria aqui que você implementaria o bloom e outros pós-efeitos.

Um aprimoramento de desempenho comum para esta técnica (se a cena tiver muitos objetos) é usar um "pré-passe", no qual você renderiza todos os objetos sem desenhar nada no buffer de estrutura de cores (use glColorMaskpara desativar a gravação de cores). Isso apenas preenche o buffer de profundidade. Dessa forma, se você renderizar um objeto que está atrás de outro, a GPU poderá pular rapidamente esses fragmentos. Ele ainda precisa executar o shader de vértice, mas pode pular os cálculos de shader de fragmento normalmente mais caros.

É mais simples de codificar e mais fácil de visualizar. E em alguns hardwares (principalmente GPUs móveis e incorporados), pode ser mais eficiente que a alternativa. Mas no hardware de ponta, a alternativa geralmente vence em cenas com muitas luzes.

Renderização adiada

A renderização adiada é um pouco mais complicada.

A equação de iluminação usada para calcular a luz de um ponto em uma superfície usa os seguintes parâmetros de superfície:

  • Posição da superfície
  • Normais de superfície
  • Cor difusa da superfície
  • Cor especular da superfície
  • Brilho especular de superfície
  • Possivelmente outros parâmetros de superfície (dependendo da complexidade da sua equação de iluminação)

Na renderização direta, esses parâmetros atingem a função de iluminação do shader de fragmento, passando diretamente do shader de vértice, sendo puxados de texturas (geralmente através de coordenadas de textura passadas do shader de vértice) ou gerados a partir de um pano inteiro no shader de fragmento, com base em outros parâmetros. A cor difusa pode ser calculada combinando uma cor por vértice com uma textura, combinando várias texturas, qualquer que seja.

Na renderização adiada, tornamos tudo isso explícito. Na primeira passagem, renderizamos todos os objetos. Mas nós não renderizamos cores . Em vez disso, renderizamos parâmetros de superfície . Portanto, cada pixel na tela possui um conjunto de parâmetros de superfície. Isso é feito via renderização para texturas fora da tela. Uma textura armazenaria a cor difusa como seu RGB e, possivelmente, o brilho especular como o alfa. Outra textura armazenaria a cor especular. Um terço armazenaria o normal. E assim por diante.

A posição geralmente não é armazenada. Em vez disso, é reconstituído na segunda passagem pela matemática, que é muito complexa para entrar aqui. Basta dizer que usamos o buffer de profundidade e a posição do fragmento do espaço da tela como entrada para descobrir a posição do espaço da câmera do ponto em uma superfície.

Portanto, agora que essas texturas contêm essencialmente todas as informações de superfície para cada pixel visível na cena, começamos a renderizar quads em tela cheia. Cada luz recebe uma renderização quádrupla em tela cheia. Amostramos as texturas dos parâmetros de superfície (e reconstituímos a posição) e, em seguida, apenas as usamos para calcular a contribuição dessa luz. Isso é adicionado (novamente glBlendFunc(GL_ONE, GL_ONE)) à imagem. Continuamos fazendo isso até ficar sem luz.

HDR novamente é uma etapa pós-processo.

A maior desvantagem da renderização adiada é o antialiasing. Requer um pouco mais de trabalho para antialias corretamente.

A maior vantagem, se sua GPU tiver muita largura de banda de memória, é o desempenho. Nós renderizamos a geometria real apenas uma vez (ou 1 + 1 por luz que possui sombras, se estivermos fazendo um mapeamento de sombras). Nós não gastar todo o tempo em pixels ou geometria escondidos que não é visível depois disso. Todo o tempo gasto na iluminação é gasto em coisas que são realmente visíveis.

Se sua GPU não possui muita largura de banda de memória, o passe de luz pode realmente começar a doer. Não é divertido extrair de 3 a 5 texturas por pixel da tela.

Pré-passe leve

Essa é uma espécie de variação na renderização diferida que possui vantagens e desvantagens interessantes.

Assim como na renderização adiada, você processa seus parâmetros de superfície em um conjunto de buffers. No entanto, você abreviou os dados da superfície; os únicos dados de superfície com os quais você se preocupa nesse momento são o valor do buffer de profundidade (para reconstruir a posição), normal e o brilho especular.

Então, para cada luz, você calcula apenas os resultados da iluminação. Sem multiplicação com cores de superfície, nada. Apenas o ponto (N, L) e o termo especular, completamente sem as cores da superfície. Os termos especulares e difusos devem ser mantidos em buffers separados. Os termos especulares e difusos para cada luz são resumidos nos dois buffers.

Em seguida, você renderiza novamente a geometria, usando os cálculos totais de iluminação especular e difusa para fazer a combinação final com a cor da superfície, produzindo assim a refletância geral.

As vantagens aqui são que você recebe multisampling de volta (pelo menos, mais fácil do que com adiado). Você faz menos renderização por objeto do que a renderização direta. Mas o principal adiado que isso fornece é um momento mais fácil para ter diferentes equações de iluminação para diferentes superfícies.

Com a renderização adiada, você geralmente desenha a cena inteira com o mesmo sombreador por luz. Portanto, todo objeto deve usar os mesmos parâmetros de material. Com o pré-passe da luz, você pode atribuir a cada objeto um sombreador diferente, para que ele possa executar sozinho a etapa final da iluminação.

Isso não oferece tanta liberdade quanto o caso de renderização direta. Mas ainda é mais rápido se você tiver a largura de banda de textura disponível.

Nicol Bolas
fonte
-1: falha ao mencionar LPP / PPL. -1 adiado: a renderização é uma vitória instantânea em qualquer hardware DX9.0 (sim, mesmo no meu laptop 'comercial') - que é um requisito básico desde 2009. A menos que você esteja direcionando o DX8.0 (que não pode adiar / LPP) Adiado / LPP é o padrão . Finalmente, 'muita largura de banda de memória' é insana - geralmente não estamos saturando o PCI-X x4, mas, além disso, o LPP reduz substancialmente a largura de banda da memória. Finalmente, -1 para o seu comentário; loops como este OK? Você sabe que esses loops estão acontecendo 2073600 vezes por quadro, certo? Mesmo com o parrelismo da placa gráfica, é ruim.
Jonathan Dickinson
1
@ JonathanDickinson Acho que o argumento dele era que a largura de banda da memória para o pré-passe diferido / leve é ​​normalmente várias vezes maior do que para a renderização direta. Isso não invalida a abordagem adiada; é apenas algo a considerar ao escolher. BTW: seus buffers diferidos devem estar na memória de vídeo, portanto a largura de banda do PCI-X é irrelevante; é a largura de banda interna da GPU que importa. Os shaders de pixel longo, por exemplo, com um loop desenrolado, não são motivo de pânico se estiverem fazendo um trabalho útil. E não há nada errado com o truque do pré-buffer z-buffer; Funciona bem.
Nathan Reed
3
@ JonathanDickinson: Isso está falando do WebGL, então qualquer discussão sobre "modelos shader" é irrelevante. E que tipo de renderização usar não é um "tópico religioso": é simplesmente uma questão de qual hardware você está executando. Uma GPU incorporada, onde "memória de vídeo" é apenas RAM normal da CPU, funcionará muito mal com a renderização adiada. Em um renderizador baseado em bloco móvel, é ainda pior . A renderização adiada não é uma "vitória instantânea", independentemente do hardware; ele tem suas vantagens, assim como qualquer hardware.
Nicol Bolas
2
@ JonathanDickinson: "Além disso, com o truque de pré-passe do buffer z, você lutará para eliminar o z-fighting com os objetos que devem ser desenhados." Isso é total absurdo. Você está renderizando os mesmos objetos com as mesmas matrizes de transformação e o mesmo sombreador de vértice. A renderização multipass foi realizada no Voodoo 1 dias; este é um problema resolvido . Acumular iluminação não faz nada para mudar isso.
Nicol Bolas
8
@ JonathanDickinson: Mas não estamos falando sobre renderizar uma estrutura de arame, estamos? Estamos falando de renderizar os mesmos triângulos de antes. O OpenGL garante invariância para o mesmo objeto que está sendo renderizado (desde que você esteja usando o mesmo sombreador de vértice, é claro, e mesmo assim, existe a invariantpalavra-chave para garantir isso para outros casos).
Nicol Bolas
4

Você precisa usar renderização adiada ou iluminação pré-passe . Alguns dos antigos dutos de função fixa (leia-se: sem sombreadores) suportam até 16 ou 24 luzes - mas é isso . A renderização adiada elimina o limite de luz; mas à custa de um sistema de renderização muito mais complicado.

Aparentemente, o WebGL suporta MRT, que é absolutamente necessário para qualquer forma de renderização adiada - portanto, é possível; Só não tenho certeza de quão plausível é.

Como alternativa, você pode investigar o Unity 5 - que adiou a renderização imediatamente.

Outra maneira simples de lidar com isso é simplesmente priorizar as luzes (talvez, com base na distância do jogador e se elas estão no compartimento da câmera) e habilitar apenas o top 8. Muitos títulos AAA conseguiram fazer isso sem muito impacto na qualidade da saída (por exemplo, Far Cry 1).

Você também pode procurar mapas de luz pré-calculados . Jogos como o Quake 1 têm muita milhagem a partir deles - e podem ser bem pequenos (a filtragem bilinear suaviza bastante os mapas de luz esticados). Infelizmente, o pré-cálculo exclui a noção de luzes 100% dinâmicas, mas realmente parece ótimo . Você pode combinar isso com seu limite de 8 luzes, por exemplo, apenas foguetes ou algo assim teriam uma luz real - mas as luzes na parede ou tais seriam mapas de luz.

Nota lateral: você não quer fazer um loop sobre eles em um shader? Diga adeus ao seu desempenho. Uma GPU não é uma CPU e não foi projetada para funcionar da mesma maneira que, por exemplo, o JavaScript. Lembre-se de que cada pixel que você renderiza (mesmo que seja substituído) deve executar o loop - portanto, se você executar a 1920x1080 e um loop simples que executa 16 vezes, você estará executando efetivamente tudo dentro desse loop 33177600 vezes. Sua placa de vídeo executará muitos desses fragmentos em paralelo, mas esses loops ainda consumirão hardware mais antigo.

Jonathan Dickinson
fonte
-1: "Você precisa usar a renderização adiada" Isso não é verdade. A renderização adiada é certamente uma maneira de fazê-lo, mas não é a única . Também os loops não são tão ruins em termos de desempenho, especialmente se forem baseados em valores uniformes (ou seja: cada fragmento não possui um comprimento de loop diferente).
Nicol Bolas
1
Por favor, leia o quarto parágrafo.
Jonathan Dickinson
2

Você pode usar um sombreador de pixel que suporta n luzes (onde n é um número pequeno como 4 ou 8) e redesenhar a cena várias vezes, passando um novo lote de luzes a cada vez e usando a mistura aditiva para combiná-las.

Essa é a ideia básica. É claro que existem muitas otimizações necessárias para tornar isso rápido o suficiente para uma cena de tamanho razoável. Não apague todas as luzes, apenas as visíveis (frustum e seleção de oclusão); na verdade, não redesenhe a cena inteira a cada passagem, apenas os objetos ao alcance das luzes dessa passagem; tenha várias versões do shader que suportam diferentes números de luzes (1, 2, 3, ...) para que você não perca tempo avaliando mais luzes do que precisa.

A renderização adiada, conforme mencionado na outra resposta, é uma boa opção quando você tem muitas luzes pequenas, mas não é a única maneira.

Nathan Reed
fonte