Sim, mas você precisa de uma mudança de paradigma.
O que você está acostumado é chamado de renderização direta. Você envia sua geometria e prossegue imediatamente com o passe de sombreamento. Na renderização básica básica, você pode fazer um loop dentro do shader para cada luz ou executar um passe por luz e misturar o resultado (com mistura aditiva).
Mas as coisas evoluíram bastante. Entra: renderização adiada
Agora que existem tantas variantes que as descrevem em detalhes, será muito mais do que aceitável para uma resposta aqui. Então, aqui vou descrever a essência do sombreamento adiado. Existem muitos outros recursos que você pode encontrar facilmente usando o google. Espero que, depois de ler isso, você tenha as palavras-chave certas para encontrar o que precisa.
A idéia básica é adiar o sombreamento para depois do pipeline. Você tem duas etapas principais:
- Renderize sua geometria e todas as informações necessárias para sombreamento em vários destinos de renderização. Isso significa que, tipicamente em uma implementação básica, você teria um buffer de profundidade, um buffer contendo as normais da sua geometria e a cor do albedo. Você logo descobrirá que precisa de outras informações sobre os materiais (por exemplo, rugosidade, fator "metálico" etc.).
Esta imagem da wikipedia mostra três buffer (cores, profundidade e normais)
Novamente, a quantidade, o tipo e o conteúdo dos buffers usados variam bastante entre os diferentes projetos. Você encontrará o conjunto de buffers com o nome de GBuffers.
- Após este é o momento de aplicar a iluminação real. Durante o passe de iluminação para cada luz, você deseja desenhar um volume de luz que depende do tipo de luz:
- Para uma luz direcional, você renderiza um quad em tela cheia.
- Para uma luz pontual, você renderiza uma esfera em que o raio se baseia na atenuação da luz pontual.
- Para uma luz spot, você renderiza um cone cujas dimensões dependem novamente das características da sua luz.
No sombreador de pixels deste passe, você passa seus GBuffers e executa sua iluminação e sombreamento usando as informações neles. Dessa forma, você processa apenas os pixels afetados por cada uma das luzes com uma velocidade sensata se comparada à renderização direta clássica.
Ele também tem várias desvantagens, principalmente o manuseio de objetos transparentes e maior consumo de largura de banda e memória de vídeo. Mas também é mais complicado lidar com vários modelos de materiais.
Você tem outras vantagens colaterais (como ter muitas informações prontas para o pós-processamento) e também é muito fácil de implementar. Mas isso não é a coisa mais legal por aí para muitas luzes.
As técnicas mais recentes são, por exemplo, as de renderização lado a lado. A idéia principal é subdividir a cena no espaço da tela "tiles" e atribuir a cada tile as luzes que o afetam. Isso existe tanto de maneira adiada quanto para a frente. Essas técnicas levam a alguns problemas quando você tem várias descontinuidades de profundidade em um bloco, mas geralmente é mais rápido que o clássico adiado e resolve vários problemas dele. Por exemplo, entre as vantagens, com diferido em mosaico, você lê os GBuffers uma vez por fragmento aceso e os pixels no mesmo mosaico processam coerentemente as mesmas luzes.
Uma evolução adicional desse lado é o sombreamento em cluster, que é conceitualmente semelhante às abordagens baseadas em ladrilhos, tendo, em vez de blocos de espaço na tela, clusters com uma extensão 3D Esse método lida melhor com o problema das descontinuidades de profundidade e geralmente tem um desempenho melhor do que os métodos lado a lado.
NOTA IMPORTANTE: Descrevi o básico do sombreamento adiado. Existem várias variações, otimizações e melhorias, então peço que você experimente uma versão simples e faça algumas pesquisas sobre outras técnicas, como a que mencionei acima.