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.
Respostas:
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 valorVocê 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
glColorMask
para 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:
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.
fonte
invariant
palavra-chave para garantir isso para outros casos).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.
fonte
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.
fonte