Pseudo Lens Flare

Olá Habr! Apresento a você a tradução do artigo “Pseudo Lens Flare” de John Chapman.

imagem

O reflexo de lente ( reflexo de lente) é um artefato fotográfico resultante da dispersão e refração da luz em um sistema de lentes. Embora seja um artefato, há muitas razões para usar o reflexo de lente em gráficos de computador:

  • aumenta o brilho percebido e a faixa dinâmica visível da imagem.
  • reflexo de lente é freqüentemente encontrado em fotografias, então sua ausência pode ser notável
  • pode desempenhar um papel importante no estilo ou no drama, ou pode fazer parte da jogabilidade dos jogos (imagine o brilho que cega um jogador)

Tradicionalmente, o reflexo de lente em tempo real é implementado usando tecnologias baseadas em sprites. Embora os sprites apresentem resultados facilmente controlados e muito realistas, eles devem ser colocados explicitamente e exigir que os dados de oclusão sejam exibidos corretamente. Aqui, descreverei um efeito de espaço na tela simples e relativamente barato que cria uma pseudo lente reflexa a partir do buffer de cores de entrada. Como não é baseado na física, o resultado é ligeiramente diferente do foto-realista, mas pode ser usado em combinação com (ou como um substituto) para efeitos tradicionais baseados em sprites.

Algoritmo


Consiste em 4 etapas:

  1. Downsample / limiar.
  2. Geração de elementos de reflexo de lente .
  3. Desfocar
  4. Upscale / mistura com a imagem original.

1. Downsample / Threshold


Downsampling - otimização para reduzir o custo das etapas subseqüentes. Além disso, queremos selecionar um subconjunto dos pixels mais brilhantes na imagem original. O uso de escala / viés (escala / viés) fornece uma maneira flexível de conseguir isso:

uniform sampler2D uInputTex; uniform vec4 uScale; uniform vec4 uBias; noperspective in vec2 vTexcoord; out vec4 fResult; void main() { fResult = max(vec4(0.0), texture(uInputTex, vTexcoord) + uBias) * uScale; } 

imagem

O ajuste de escala / viés é a principal maneira de ajustar o efeito; as melhores configurações dependerão da faixa dinâmica do buffer de cores e da espessura desejada para ver o resultado. Devido ao fato de a técnica ser uma aproximação, é mais provável que a sutileza pareça melhor.

2. Geração de elementos de reflexo de lente


Os elementos de reflexo da lente tendem a girar sobre o centro da imagem. Simulando esse efeito, podemos expandir o resultado do estágio anterior horizontalmente / verticalmente. Isso é fácil de fazer no estágio de geração do elemento, expandindo as coordenadas de textura:

 vec2 texcoord = -vTexcoords + vec2(1.0); 

Isso não é necessário; a geração de elementos funciona bem com e sem ele. No entanto, o resultado da alteração das coordenadas da textura ajuda a separar visualmente o efeito de reflexo da lente da imagem original.

Fantasmas


Fantasmas ” (fantasmas) estão repetindo destaques que refletem as áreas brilhantes no buffer de cores, que se desdobram em relação ao centro da imagem. A abordagem que escolhi gerar é obter um vetor do pixel atual para o centro da tela e, em seguida, fazer várias seleções ao longo desse vetor.

imagem

 uniform sampler2D uInputTex; uniform int uGhosts; // number of ghost samples uniform float uGhostDispersal; // dispersion factor noperspective in vec2 vTexcoord; out vec4 fResult; void main() { vec2 texcoord = -vTexcoord + vec2(1.0); vec2 texelSize = 1.0 / vec2(textureSize(uInputTex, 0)); // ghost vector to image centre: vec2 ghostVec = (vec2(0.5) - texcoord) * uGhostDispersal; // sample ghosts: vec4 result = vec4(0.0); for (int i = 0; i < uGhosts; ++i) { vec2 offset = fract(texcoord + ghostVec * float(i)); result += texture(uInputTex, offset); } fResult = result; } 

Observe que eu uso fract () para garantir que as coordenadas da textura se enrolem; De maneira equivalente, você pode usar o modo de quebra automática GL_REPEAT para textura.

Aqui está o resultado:

imagem

Você pode melhorar o resultado, permitindo que apenas áreas brilhantes próximas ao centro da imagem gerem fantasmas. Podemos conseguir isso adicionando pesos que diminuirão do centro para amostras:

 vec4 result = vec4(0.0); for (int i = 0; i < uGhosts; ++i) { vec2 offset = fract(texcoord + ghostVec * float(i)); float weight = length(vec2(0.5) - offset) / length(vec2(0.5)); weight = pow(1.0 - weight, 10.0); result += texture(uInputTex, offset) * weight; } 

A função de peso é a mais simples possível - linear. A razão pela qual calculamos o peso dentro do loop é porque as áreas brilhantes no centro da imagem de entrada podem "lançar" fantasmas para as bordas, mas as áreas brilhantes nas bordas não podem projetar fantasmas para o centro.

imagem

A melhoria final é a alteração radial da cor do fantasma, de acordo com a textura 1D:

imagem

É aplicado após o ciclo para afetar a cor final do fantasma:

 result *= texture(uLensColor, length(vec2(0.5) - texcoord) / length(vec2(0.5))); 

HALOS (halos)


Se levarmos o vetor para o centro da imagem, como no cálculo fantasma , mas fixarmos o comprimento do vetor, obteremos um efeito diferente: a imagem original é deformada radialmente:

imagem
Podemos usar isso para criar um "halo" multiplicando o peso por uma amostra, limitando assim a contribuição da imagem deformada para um anel cujo raio é controlado pelo uHaloWidth :

 // sample halo: vec2 haloVec = normalize(ghostVec) * uHaloWidth; float weight = length(vec2(0.5) - fract(texcoord + haloVec)) / length(vec2(0.5)); weight = pow(1.0 - weight, 5.0); result += texture(uInputTex, texcoord + haloVec) * weight; 

imagem

DISTORÇÃO CROMÁTICA (distorção de cor)


Alguns reflexos de lente têm distorção de cor causada por variações na refração da luz em diferentes comprimentos de onda. Podemos simular isso criando uma função que seleciona os canais vermelho, verde e azul separadamente com deslocamentos ligeiramente diferentes ao longo do vetor de amostra:

 vec3 textureDistorted( in sampler2D tex, in vec2 texcoord, in vec2 direction, // direction of distortion in vec3 distortion // per-channel distortion factor ) { return vec3( texture(tex, texcoord + direction * distortion.r).r, texture(tex, texcoord + direction * distortion.g).g, texture(tex, texcoord + direction * distortion.b).b ); } 

Ele pode ser usado como um substituto direto para chamar texture () na listagem anterior. Calculo a direção e a distorção da seguinte maneira:

 vec2 texelSize = 1.0 / vec2(textureSize(uInputTex, 0)); vec3 distortion = vec3(-texelSize.x * uDistortion, 0.0, texelSize.x * uDistortion); vec3 direction = normalize(ghostVec); 

Embora a função de busca seja simples, ela custa x3 amostras da textura, embora todas devam ser compatíveis com o cache, a menos que você defina uDistortion com algum valor gigantesco.

Com a geração de elementos, tudo. Aqui está o resultado:

imagem

3. Desfoque


Sem desfocagem, os elementos de reflexo da lente (em particular, fantasmas) tendem a preservar a aparência da imagem. Ao adicionar o desfoque aos elementos de reflexo da lente , enfraquecemos as altas frequências e, assim, reduzimos o contraste com a imagem de entrada, o que nos ajuda a vender o efeito.

imagem

Não direi como fazer um borrão; Você pode ler sobre isso em vários recursos da Internet (desfoque gaussiano).

4. Upscale / misture com a imagem original


Portanto, temos nossos elementos de reflexo de lente bem borrados. Como podemos combiná-los com a imagem original? Há várias considerações importantes sobre todo o pipeline de renderização:

  • Qualquer desfoque de movimento subsequente ou profundidade de campo deve ser aplicada antes da combinação com reflexo de lente , para que os elementos de reflexo de lente não participem desses efeitos.
  • O reflexo da lente deve ser aplicado antes de qualquer mapeamento de tom . Isso faz sentido físico, pois o mapeamento de tom imita a resposta do filme / CMOS à luz recebida, da qual o reflexo da lente é parte integrante.

Com isso em mente, há algumas coisas que podemos fazer nesta fase para melhorar o resultado:

SUJEIRA DA LENTE


Primeiro, você precisa modificar os elementos de reflexo da lente com uma textura suja em resolução total (amplamente utilizada no Battlefield 3):

imagem

 uniform sampler2D uInputTex; // source image uniform sampler2D uLensFlareTex; // input from the blur stage uniform sampler2D uLensDirtTex; // full resolution dirt texture noperspective in vec2 vTexcoord; out vec4 fResult; void main() { vec4 lensMod = texture(uLensDirtTex, vTexcoord); vec4 lensFlare = texture(uLensFlareTex, vTexcoord) * lensMod; fResult = texture(uInputTex, vTexcoord) + lensflare; } 

A chave para isso é a textura muito suja nas lentes. Se o contraste for baixo, as formas de reflexo da lente tendem a dominar o resultado. À medida que o contraste aumenta, os elementos de reflexo da lente são abafados, o que dá uma aparência estética diferente e também oculta alguns defeitos.

DIFFRAÇÃO STARBURST


Como uma melhoria adicional, podemos usar a textura starburst adicionando-a à sujeira da lente :

imagem
Como textura, a explosão de estrelas não parece muito boa. No entanto, podemos passar a matriz de transformação para o shader, o que nos permitirá girar / deformar a explosão de estrela a cada quadro e obter o efeito dinâmico desejado:

 uniform sampler2D uInputTex; // source image uniform sampler2D uLensFlareTex; // input from the blur stage uniform sampler2D uLensDirtTex; // full resolution dirt texture uniform sampler2D uLensStarTex; // diffraction starburst texture uniform mat3 uLensStarMatrix; // transforms texcoords noperspective in vec2 vTexcoord; out vec4 fResult; void main() { vec4 lensMod = texture(uLensDirtTex, vTexcoord); vec2 lensStarTexcoord = (uLensStarMatrix * vec3(vTexcoord, 1.0)).xy; lensMod += texture(uLensStarTex, lensStarTexcoord); vec4 lensFlare = texture(uLensFlareTex, vTexcoord) * lensMod; fResult = texture(uInputTex, vTexcoord) + lensflare; } 

A matriz de transformação uLensStarMatrix é baseada no valor obtido da orientação da câmera, como a seguir:

 vec3 camx = cam.getViewMatrix().col(0); // camera x (left) vector vec3 camz = cam.getViewMatrix().col(1); // camera z (forward) vector float camrot = dot(camx, vec3(0,0,1)) + dot(camz, vec3(0,1,0)); 

Existem outras maneiras de obter o valor do camrot; mais importante, ele deve mudar continuamente quando a câmera é girada. A matriz em si é construída da seguinte maneira:

 mat3 scaleBias1 = ( 2.0f, 0.0f, -1.0f, 0.0f, 2.0f, -1.0f, 0.0f, 0.0f, 1.0f, ); mat3 rotation = ( cos(camrot), -sin(camrot), 0.0f, sin(camrot), cos(camrot), 0.0f, 0.0f, 0.0f, 1.0f ); mat3 scaleBias2 = ( 0.5f, 0.0f, 0.5f, 0.0f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, ); mat3 uLensStarMatrix = scaleBias2 * rotation * scaleBias1; 

As matrizes de escala e polarização precisam de compensações da origem da textura, para que possamos girar a explosão estelar em relação ao centro da imagem.

Conclusão


Então agora tudo! Este método demonstra como um processo pós-simplificado oferece um reflexo decente nas lentes . Não é totalmente fotorrealista, mas se usado corretamente, pode produzir excelentes resultados.

imagem



UPD
O autor também publicou um artigo com pequenas otimizações.
O código fonte pode ser visto aqui e aqui .

Source: https://habr.com/ru/post/pt439408/


All Articles