Aprende OpenGL. Lección 5.8 - Bloom

OGL3

Bloom


Debido al rango de brillo limitado disponible para los monitores convencionales, la tarea de mostrar convincentemente fuentes de luz brillante y superficies iluminadas es difícil por definición. Uno de los métodos comunes para resaltar áreas brillantes en el monitor es una técnica que agrega un halo de brillo alrededor de los objetos brillantes, dando la impresión de "propagación" de la luz fuera de la fuente de luz. Como resultado, el observador da la impresión de un alto brillo de tales áreas iluminadas o fuentes de luz.

El efecto descrito de un halo y la salida de luz más allá de la fuente se logra mediante una técnica de procesamiento posterior llamada bloom . La aplicación del efecto agrega un halo de brillo característico a todas las áreas brillantes de la escena mostrada, que se puede ver en el siguiente ejemplo:



Bloom agrega una pista visual distintiva a la imagen sobre el brillo significativo de los objetos cubiertos por el halo del efecto aplicado. Al aplicarse de manera selectiva y en un grado preciso (que muchos juegos, por desgracia, no pueden hacer frente), el efecto puede mejorar significativamente la expresividad visual de la iluminación utilizada en la escena, así como agregar drama en ciertas situaciones.

Esta técnica funciona en conjunto con el renderizado HDR casi como una adición evidente. Aparentemente, debido a esto, muchas personas mezclan erróneamente estos dos términos con la plena intercambiabilidad. Sin embargo, estas técnicas son completamente independientes y se utilizan para diferentes propósitos. Es posible implementar bloom utilizando el buffer de fotogramas predeterminado con una profundidad de color de 8 bits, al igual que aplicar el renderizado HDR sin recurrir al uso de bloom. Lo único es que el renderizado HDR le permite implementar el efecto de una manera más eficiente (lo veremos más adelante).

Para implementar la floración, la escena iluminada se representa primero de la manera habitual. A continuación, se extrae un búfer de color HDR y un búfer de color que contiene solo partes brillantes de la escena. Esta imagen de porción brillante extraída se borra y se superpone encima de la imagen HDR original de la escena.

Para hacerlo más claro, analizaremos el proceso paso a paso. Renderice una escena que contenga 4 fuentes de luz brillante que se muestran como cubos de colores. Todos ellos tienen un valor de brillo en el rango de 1.5 a 15.0. Si el búfer de color se emite al HDR, el resultado es el siguiente:


De este búfer de color HDR, extraemos todos los fragmentos cuyo brillo excede un límite predeterminado. Resulta una imagen que contiene solo áreas iluminadas:


Además, esta imagen de áreas brillantes es borrosa. La gravedad del efecto está determinada esencialmente por la fuerza y ​​el radio del filtro de desenfoque aplicado:


La imagen borrosa resultante de áreas brillantes es la base del efecto final de halos alrededor de objetos brillantes. Esta textura simplemente se mezcla con la imagen HDR original de la escena. Dado que las áreas brillantes estaban borrosas, sus tamaños aumentaron, lo que finalmente da un efecto visual de luminosidad que va más allá de los límites de las fuentes de luz:


Como puede ver, la floración no es la técnica más sofisticada, pero lograr su alta calidad visual y confiabilidad no siempre es fácil. En su mayor parte, el efecto depende de la calidad y el tipo de filtro de desenfoque aplicado. Incluso pequeños cambios en los parámetros del filtro pueden cambiar drásticamente la calidad final del equipo.

Entonces, las acciones anteriores nos dan un algoritmo paso a paso del efecto de post-procesamiento para el efecto de floración. La imagen a continuación resume las acciones requeridas:


En primer lugar, necesitamos información sobre las partes brillantes de la escena en función de un valor umbral dado. Esto es lo que haremos.

Extraer destacados


Entonces, para empezar, necesitamos obtener dos imágenes basadas en nuestra escena. Sería ingenuo renderizar dos veces, pero use el método más avanzado de Objetivos de renderizado múltiple ( MRT ): especificamos más de una salida en el sombreador de fragmentos final, y gracias a esto, ¡se pueden extraer dos imágenes en una sola pasada! Para especificar en qué búfer de color se generará el sombreador, se utiliza el especificador de diseño :

layout (location = 0) out vec4 FragColor; layout (location = 1) out vec4 BrightColor; 

Por supuesto, el método solo funcionará si hemos preparado varios buffers para escribir. En otras palabras, para implementar múltiples salidas desde el sombreador de fragmentos, el búfer de cuadros utilizado en este momento debe contener un número suficiente de búferes de color conectados. Si pasamos a la lección sobre el búfer de cuadros , entonces se recuerda que al unir la textura como un búfer de color, podríamos indicar el número de archivo adjunto de color . Hasta ahora, no necesitábamos usar un archivo adjunto que no sea GL_COLOR_ATTACHMENT0 , pero esta vez GL_COLOR_ATTACHMENT1 será útil, porque necesitamos dos objetivos para grabar a la vez:

 //       unsigned int hdrFBO; glGenFramebuffers(1, &hdrFBO); glBindFramebuffer(GL_FRAMEBUFFER, hdrFBO); unsigned int colorBuffers[2]; glGenTextures(2, colorBuffers); for (unsigned int i = 0; i < 2; i++) { glBindTexture(GL_TEXTURE_2D, colorBuffers[i]); glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL ); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); //     glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D, colorBuffers[i], 0 ); } 

Además, al llamar a glDrawBuffers , deberá decirle explícitamente a OpenGL que vamos a enviar a varios buffers. De lo contrario, la biblioteca solo se enviará al primer archivo adjunto, ignorando las operaciones de escritura en otros archivos adjuntos. Como argumento para la función, se pasa una matriz de identificadores de los archivos adjuntos utilizados de la enumeración correspondiente:

 unsigned int attachments[2] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 }; glDrawBuffers(2, attachments); 

Para este búfer de cuadros, cualquier sombreador de fragmentos que especifique un especificador de ubicación para sus salidas escribirá en el búfer de color correspondiente. Y esta es una gran noticia, porque de esta manera evitamos el pase de renderizado innecesario para extraer datos sobre las partes brillantes de la escena: puede hacer todo de una vez en un solo sombreador:

 #version 330 core layout (location = 0) out vec4 FragColor; layout (location = 1) out vec4 BrightColor; [...] void main() { [...] //      FragColor = vec4(lighting, 1.0); //         //   -    ,    float brightness = dot(FragColor.rgb, vec3(0.2126, 0.7152, 0.0722)); if(brightness > 1.0) BrightColor = vec4(FragColor.rgb, 1.0); else BrightColor = vec4(0.0, 0.0, 0.0, 1.0); } 

En este fragmento, se omite la parte que contiene el código típico para calcular la iluminación. Su resultado se escribe en la primera salida del sombreador: la variable FragColor . A continuación, el color resultante del fragmento se usa para calcular el valor del brillo. Para esto, se realiza una traducción ponderada en escala de grises (por multiplicación escalar, multiplicamos los componentes correspondientes de los vectores y los sumamos, lo que lleva a un solo valor). Luego, cuando se excede el brillo de un fragmento de cierto umbral, registramos su color en la segunda salida del sombreador. Para los cubos que reemplazan las fuentes de luz, este sombreador también se ejecuta.

Habiendo descubierto el algoritmo, podemos entender por qué esta técnica funciona tan bien con el renderizado HDR. El renderizado en formato HDR permite que los componentes de color vayan más allá del límite superior de 1.0, lo que le permite ajustar de manera más flexible el umbral de brillo fuera del intervalo estándar [0., 1.], proporcionando la capacidad de ajustar qué secciones de la escena se consideran brillantes. Sin usar HDR, tendrá que contentarse con un umbral de brillo en el intervalo [0., 1.], que es bastante aceptable, pero conduce a un corte de brillo más "agudo", que a menudo hace que la floración sea demasiado intrusiva y llamativa (imagínese en un campo de nieve en lo alto de las montañas) .

Después de ejecutar el sombreador, dos buffers de destino contendrán una imagen normal de la escena, así como una imagen que contenga solo áreas brillantes.


La imagen de las áreas brillantes ahora debe procesarse con desenfoque. Puede lograr esto con un filtro rectangular simple ( cuadro ), que se utilizó en la sección de postprocesamiento de la lección de búfer de marco . Pero se obtiene un resultado mucho mejor mediante el filtrado de Gauss .

Desenfoque gaussiano


La lección posterior al procesamiento nos dio una idea de desenfoque utilizando un promedio simple de color de los fragmentos de imagen adyacentes. Este método de desenfoque es simple, pero la imagen resultante puede parecer más atractiva. El desenfoque gaussiano se basa en la curva de distribución en forma de campana del mismo nombre: los valores altos de la función se encuentran más cerca del centro de la curva y caen a ambos lados de la misma. Matemáticamente, una curva gaussiana se puede expresar con diferentes parámetros, pero la forma general de la curva sigue siendo la siguiente:


El desenfoque con pesos basados ​​en los valores de la curva de Gauss se ve mucho mejor que un filtro rectangular: debido al hecho de que la curva tiene un área más grande en la vecindad de su centro, que corresponde a pesos más grandes para fragmentos cerca del centro del núcleo del filtro. Tomando, por ejemplo, el núcleo de 32x32, usaremos los factores de ponderación cuanto más pequeño, más lejos esté el fragmento del central. Es esta característica del filtro la que proporciona un resultado de desenfoque gaussiano visualmente más satisfactorio.

La implementación del filtro requerirá una matriz bidimensional de coeficientes de ponderación, que podría rellenarse sobre la base de la expresión bidimensional que describe la curva gaussiana. Sin embargo, nos encontraremos inmediatamente con un problema de rendimiento: ¡incluso un núcleo de desenfoque relativamente pequeño en un fragmento de 32x32 requerirá 1024 muestras de textura para cada fragmento de la imagen procesada!

Afortunadamente para nosotros, la expresión de la curva gaussiana tiene una característica matemática muy conveniente: la separabilidad, que permitirá hacer dos expresiones unidimensionales a partir de una expresión bidimensional que describa los componentes horizontal y vertical. Esto permitirá desenfocar a su vez en dos enfoques: horizontalmente y luego verticalmente con conjuntos de pesos correspondientes a cada una de las direcciones. La imagen resultante será la misma que cuando se procesa un algoritmo bidimensional, pero requerirá mucha menos potencia de procesamiento del procesador de video: en lugar de 1024 muestras de la textura, ¡solo necesitamos 32 + 32 = 64! Esta es la esencia de la filtración gaussiana de dos pasos.


Para nosotros, todo esto significa una cosa: el desenfoque de una imagen tendrá que hacerse dos veces, y aquí el uso de objetos de frame buffer será útil. Aplicamos la llamada técnica de ping-pong: hay un par de objetos de búfer de cuadro y el contenido del búfer de color de un búfer de cuadro se procesa en el búfer de color del búfer de cuadro actual, luego se intercambian el búfer de cuadro de origen y el receptor de cuadro de búfer y este proceso se repite un número dado de veces. De hecho, el búfer de cuadro actual para mostrar la imagen simplemente se cambia, y con él, la textura actual a partir de la cual se realiza el muestreo para el renderizado. El enfoque le permite desenfocar la imagen original colocándola en el primer búfer de cuadros, luego desenfocar el contenido del primer búfer de cuadros, colocarlo en el segundo, luego desenfocar el segundo, colocarlo en el primero y así sucesivamente.

Antes de pasar al código de ajuste del búfer de cuadros, echemos un vistazo al código de sombreador de desenfoque gaussiano:

 #version 330 core out vec4 FragColor; in vec2 TexCoords; uniform sampler2D image; uniform bool horizontal; uniform float weight[5] = float[] (0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216); void main() { //     vec2 tex_offset = 1.0 / textureSize(image, 0); //    vec3 result = texture(image, TexCoords).rgb * weight[0]; if(horizontal) { for(int i = 1; i < 5; ++i) { result += texture(image, TexCoords + vec2(tex_offset.x * i, 0.0)).rgb * weight[i]; result += texture(image, TexCoords - vec2(tex_offset.x * i, 0.0)).rgb * weight[i]; } } else { for(int i = 1; i < 5; ++i) { result += texture(image, TexCoords + vec2(0.0, tex_offset.y * i)).rgb * weight[i]; result += texture(image, TexCoords - vec2(0.0, tex_offset.y * i)).rgb * weight[i]; } } FragColor = vec4(result, 1.0); } 

Como puede ver, utilizamos una muestra bastante pequeña de coeficientes de la curva gaussiana, que se utilizan como pesos para muestras horizontal o verticalmente en relación con el fragmento actual. El código tiene dos ramas principales que dividen el algoritmo en paso vertical y horizontal en función del valor del uniforme horizontal . El desplazamiento para cada muestra se establece igual al tamaño del texel, que se define como el recíproco del tamaño de la textura (un valor de tipo vec2 devuelto por la función textureSize ()).

Cree dos búferes de cuadros que contengan un búfer de color basado en la textura:

 unsigned int pingpongFBO[2]; unsigned int pingpongBuffer[2]; glGenFramebuffers(2, pingpongFBO); glGenTextures(2, pingpongBuffer); for (unsigned int i = 0; i < 2; i++) { glBindFramebuffer(GL_FRAMEBUFFER, pingpongFBO[i]); glBindTexture(GL_TEXTURE_2D, pingpongBuffer[i]); glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL ); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, pingpongBuffer[i], 0 ); } 

Después de obtener la textura HDR de la escena y extraer la textura de las áreas brillantes, llenamos el búfer de color de uno de los pares de framebuffers preparados con la textura de brillo y comenzamos el proceso de ping-pong diez veces (cinco veces verticalmente, cinco horizontalmente):

 bool horizontal = true, first_iteration = true; int amount = 10; shaderBlur.use(); for (unsigned int i = 0; i < amount; i++) { glBindFramebuffer(GL_FRAMEBUFFER, pingpongFBO[horizontal]); shaderBlur.setInt("horizontal", horizontal); glBindTexture( GL_TEXTURE_2D, first_iteration ? colorBuffers[1] : pingpongBuffers[!horizontal] ); RenderQuad(); horizontal = !horizontal; if (first_iteration) first_iteration = false; } glBindFramebuffer(GL_FRAMEBUFFER, 0); 

En cada iteración, seleccionamos y anclamos uno de los búferes de cuadros en función de si esta iteración se desenfocará horizontal o verticalmente, y el búfer de color del otro búfer de cuadros se usará como textura de entrada para el sombreador de desenfoque. En la primera iteración, tenemos que usar explícitamente una imagen que contenga áreas brillantes ( brilloTextura ); de lo contrario, ambos framebuffers de ping-pong permanecerán vacíos. Después de diez pases, la imagen original toma la forma de cinco veces borrosa por un filtro gaussiano completo. El enfoque utilizado nos permite cambiar fácilmente el grado de desenfoque: cuantas más iteraciones de ping-pong, más fuerte será el desenfoque.

En nuestro caso, el resultado borroso se ve así:


Para completar el efecto, solo queda combinar la imagen borrosa con la imagen HDR original de la escena.

Mezcla de texturas


Teniendo a mano la textura HDR de la escena renderizada y la textura borrosa de las áreas sobreexpuestas, todo lo que necesita para darse cuenta del famoso efecto de floración o brillo es combinar estas dos imágenes. El sombreador de fragmentos final (muy similar al que se presentó en la lección sobre el formato HDR ) hace exactamente eso: mezcla de forma aditiva dos texturas:

 #version 330 core out vec4 FragColor; in vec2 TexCoords; uniform sampler2D scene; uniform sampler2D bloomBlur; uniform float exposure; void main() { const float gamma = 2.2; vec3 hdrColor = texture(scene, TexCoords).rgb; vec3 bloomColor = texture(bloomBlur, TexCoords).rgb; hdrColor += bloomColor; // additive blending //   vec3 result = vec3(1.0) - exp(-hdrColor * exposure); //     - result = pow(result, vec3(1.0 / gamma)); FragColor = vec4(result, 1.0); } 

Qué buscar: la mezcla se realiza antes de aplicar el mapeo de tonos . Esto traducirá correctamente el brillo adicional del efecto en el rango LDR ( Rango dinámico bajo ), mientras mantiene la distribución relativa del brillo en la escena.

El resultado del procesamiento: todas las áreas brillantes recibieron un notable efecto de brillo:


Los cubos que reemplazan las fuentes de luz ahora se ven mucho más brillantes y transmiten mejor la impresión de una fuente de luz. Esta escena es bastante primitiva, porque la implementación del efecto de un entusiasmo especial no causará, pero en escenas complejas con iluminación reflexiva, una floración cualitativamente realizada puede ser un elemento visual crucial que agrega drama.

El código fuente para el ejemplo está aquí .

Observo que la lección usó un filtro bastante simple con solo cinco muestras en cada dirección. Al hacer más muestras en un radio mayor o al realizar varias iteraciones del filtro, puede mejorar visualmente el efecto. Además, vale la pena decir que visualmente la calidad de todo el efecto depende directamente de la calidad del algoritmo de desenfoque utilizado. Al mejorar el filtro, puede lograr una mejora significativa y todo el efecto. Por ejemplo, un resultado más impresionante se muestra mediante la combinación de varios filtros con diferentes tamaños de núcleo o diferentes curvas gaussianas. Los siguientes son recursos adicionales de Kalogirou y EpicGames que abordan cómo mejorar la calidad de la floración modificando el desenfoque gaussiano.

Recursos Adicionales


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


All Articles