Aprende OpenGL. Lección 5.7 - HDR


Al escribir en el framebuffer, los valores del brillo de los colores se reducen al intervalo de 0.0 a 1.0. Debido a esta característica inofensiva a primera vista, siempre tenemos que elegir los valores de iluminación y colores que se ajusten a esta restricción. Este enfoque funciona y ofrece resultados decentes, pero ¿qué sucede si nos encontramos con un área particularmente brillante con muchas fuentes de luz brillante, y el brillo total excede 1.0? Como resultado, todos los valores superiores a 1.0 se convertirán a 1.0, lo que no se ve muy bien:



Dado que los valores de color se reducen a 1.0 para una gran cantidad de fragmentos, grandes áreas de la imagen se rellenan con el mismo color blanco, se pierde una cantidad significativa de detalles de la imagen y la imagen en sí comienza a verse poco natural.


La solución a este problema puede ser reducir el brillo de las fuentes de luz para que no haya fragmentos más brillantes que 1.0 en el escenario: esta no es la mejor solución, lo que obliga al uso de valores de iluminación poco realistas. El mejor enfoque es permitir que los valores de brillo excedan temporalmente el brillo de 1.0 y en el paso final cambiar los colores para que el brillo vuelva al rango de 0.0 a 1.0, pero sin pérdida de detalles de la imagen.


La pantalla de la computadora es capaz de mostrar colores con un brillo que varía de 0.0 a 1.0, pero no tenemos esa limitación al calcular la iluminación. Al permitir que los colores del fragmento sean más brillantes que la unidad, obtenemos un rango de brillo mucho más alto para el trabajo: HDR (alto rango dinámico) . Con hdr, las cosas brillantes se ven brillantes, las cosas oscuras pueden ser realmente oscuras, y al hacerlo veremos los detalles.



Inicialmente, se utilizó un alto rango dinámico en la fotografía: el fotógrafo tomó varias fotografías idénticas de la escena con diferentes exposiciones, capturando colores de casi cualquier brillo. La combinación de estas fotos forma una imagen hdr en la que la mayoría de los detalles se hacen visibles debido a la combinación de imágenes con diferentes pérdidas de exposición. Por ejemplo, a continuación, en la imagen de la izquierda, los fragmentos altamente iluminados de la imagen son claramente visibles (mira la ventana), pero estos detalles desaparecen cuando se usa una alta exposición. Sin embargo, la alta exposición hace que los detalles de las áreas oscuras de la imagen que antes no eran visibles.



Esto es similar a cómo funciona el ojo humano. Con falta de luz, el ojo se adapta, de modo que los detalles oscuros se vuelven claramente visibles, y de manera similar para áreas brillantes. Se puede decir que el ojo humano tiene un control de exposición automático, dependiendo del brillo de la escena.


El renderizado HDR funciona de la misma manera. Al renderizar, permitimos utilizar un amplio rango de valores de brillo para recopilar información sobre los detalles brillantes y oscuros de la escena, y al final convertiremos los valores del rango HDR a LDR (rango dinámico bajo, rango de 0 a 1). Esta transformación se llama mapeo de tonos , hay una gran cantidad de algoritmos destinados a preservar la mayoría de los detalles de la imagen al convertir a LDR. Estos algoritmos a menudo tienen una configuración de exposición que les permite mostrar mejor las áreas brillantes u oscuras de la imagen.


El uso de HDR al renderizar nos permite no solo superar el rango de LDR de 0 a 1 y guardar más detalles de la imagen, sino que también permite indicar el brillo real de las fuentes de luz. Por ejemplo, el sol tiene un brillo de luz mucho mayor que algo como una linterna, entonces ¿por qué no poner el sol en esto (por ejemplo, darle un brillo de 10.0)? Esto nos permitirá ajustar mejor la iluminación de la escena con parámetros de brillo más realistas, lo que sería imposible con la representación LDR y un rango de brillo de 0 a 1.


Dado que la pantalla muestra el brillo solo de 0 a 1, nos vemos obligados a convertir el rango de valores HDR utilizado nuevamente al rango del monitor. Simplemente escalar el rango no será una buena solución, ya que las áreas brillantes comenzarán a dominar la imagen. Sin embargo, podemos usar varias ecuaciones o curvas para convertir los valores HDR a LDR, lo que nos dará un control total sobre el brillo de la escena. Esta transformación se llama mapeo de tonos y es el paso final en la representación HDR.


Framebuffers de punto flotante


Para implementar la representación HDR, necesitamos una forma de evitar que los valores se lleven a un rango de 0 a 1 desde el sombreador de fragmentos. Si el framebuffer usa el formato de punto fijo normalizado (GL_RGB) para los buffers de color, entonces OpenGL limita automáticamente los valores antes de guardarlo en el framebuffer. Esta restricción se aplica a la mayoría de los formatos de framebuffer, excepto los formatos de punto flotante.


Para almacenar valores que están fuera del rango [0.0..1.0], podemos usar un búfer de color con los siguientes formatos: GL_RGB16F, GL_RGBA16F, GL_RGB32F or GL_RGBA32F . Esto es genial para renderizar hdr. Tal búfer se llamará framebuffer de punto flotante.


La creación de un búfer de punto flotante difiere de un búfer normal solo en que usa un formato interno diferente:


 glBindTexture(GL_TEXTURE_2D, colorBuffer); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGBA, GL_FLOAT, NULL); 

El framebuffer de OpenGL por defecto usa solo 8 bits para almacenar cada color. En el framebuffer de coma flotante con los formatos GL_RGB32F o GL_RGBA32F , se utilizan 32 bits para almacenar cada color, 4 veces más. Si no se requiere una precisión muy alta, el formato GL_RGBA16F será suficiente.


Si se adjunta un búfer de punto flotante al búfer de cuadro para el color, podemos representar la escena en él teniendo en cuenta que los valores de color no se limitarán al rango de 0 a 1. En el código de este artículo, primero representamos la escena en el búfer de cuadro de punto flotante y luego mostramos el contenido amortiguadores de color en un rectángulo de media pantalla. Se parece a esto:


 glBindFramebuffer(GL_FRAMEBUFFER, hdrFBO); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // [...]    hdr glBindFramebuffer(GL_FRAMEBUFFER, 0); //  hdr    2     hdrShader.use(); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, hdrColorBufferTexture); RenderQuad(); 

Aquí los valores de color contenidos en el búfer de color pueden ser mayores que 1. Para este artículo, se creó una escena con un gran cubo alargado que se parece a un túnel con cuatro fuentes de luz puntuales, una de ellas se encuentra al final del túnel y tiene un gran brillo.


 std::vector<glm::vec3> lightColors; lightColors.push_back(glm::vec3(200.0f, 200.0f, 200.0f)); lightColors.push_back(glm::vec3(0.1f, 0.0f, 0.0f)); lightColors.push_back(glm::vec3(0.0f, 0.0f, 0.2f)); lightColors.push_back(glm::vec3(0.0f, 0.1f, 0.0f)); 

La representación en el búfer de punto flotante es exactamente la misma que si estuviéramos renderizando la escena en un búfer de fotogramas normal. Lo único nuevo es el sombreador fragmentado hdr, que trata con un sombreado simple de un rectángulo de pantalla completa con valores de una textura, que es un búfer de color de punto flotante. Para comenzar, escriba un sombreador simple que transfiera los datos de entrada sin cambios:


 #version 330 core out vec4 FragColor; in vec2 TexCoords; uniform sampler2D hdrBuffer; void main() { vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb; FragColor = vec4(hdrColor, 1.0); } 

Tomamos la entrada del punto flotante del búfer de color y la usamos como el valor de salida del sombreador. Sin embargo, dado que el rectángulo 2D se representa en el framebuffer de manera predeterminada, los valores de salida del sombreador se limitarán a un intervalo de 0 a 1, a pesar de que en algunos lugares los valores son mayores que 1.



Es obvio que los valores de color demasiado grandes al final del túnel están limitados a la unidad, ya que una parte importante de la imagen es completamente blanca, y perdemos detalles de la imagen que son más brillantes que la unidad. Como utilizamos los valores HDR directamente como LDR, esto es equivalente a no tener HDR. Para solucionar esto, debemos mostrar los diferentes valores de color en el rango de 0 a 1 sin perder ningún detalle en la imagen. Para hacer esto, aplique compresión tonal.


Compresión de tono


La compresión de tono es la conversión de valores de color para ajustarlos en el rango de 0 a 1 sin perder detalles de la imagen, a menudo en combinación con dar a la imagen el balance de blancos deseado.


El algoritmo de mapeo de tonos más simple se conoce como el algoritmo de mapeo de tonos Reinhard . Muestra cualquier valor HDR en el rango LDR. Agregue este algoritmo al sombreador de fragmentos anterior y también aplique la corrección gamma (y el uso de texturas SRGB).


 void main() { const float gamma = 2.2; vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb; //   vec3 mapped = hdrColor / (hdrColor + vec3(1.0)); // - mapped = pow(mapped, vec3(1.0 / gamma)); FragColor = vec4(mapped, 1.0); } 

Nota trans. - para valores pequeños de x, la función x / (1 + x) se comporta aproximadamente como x, para x grande tiende a la unidad. Gráfico de funciones:


Con la compresión de tonos Reinhardt ya no perdemos detalles en áreas brillantes de la imagen. El algoritmo prefiere áreas brillantes, haciendo que las áreas oscuras sean menos distintas.



Aquí puede ver nuevamente los detalles al final de la imagen, como la textura de madera. Con este algoritmo relativamente simple, podemos ver claramente cualquier color del rango HDR y podemos controlar la iluminación de la escena sin perder detalles de la imagen.


Vale la pena señalar que podemos usar la compresión tonal directamente al final de nuestro sombreador para calcular la iluminación, y luego no necesitamos un bufferbuffer de punto flotante. Sin embargo, en escenas más complejas, a menudo encontrará la necesidad de almacenar valores HDR intermedios en memorias intermedias de punto flotante, por lo que esto será útil.

Otra característica interesante de la compresión de tonos es el uso de un parámetro de exposición. Puede recordar que en las imágenes al comienzo del artículo se veían varios detalles a diferentes valores de exposición. Si tenemos una escena en la que el día y la noche cambian, tiene sentido usar una exposición baja durante el día y alta durante la noche, que es similar a la adaptación del ojo humano. Con este parámetro de exposición, podemos configurar parámetros de iluminación que funcionarán día y noche en diferentes condiciones de iluminación.


Un algoritmo de compresión tonal relativamente simple con exposición se ve así:


 uniform float exposure; void main() { const float gamma = 2.2; vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb; //     vec3 mapped = vec3(1.0) - exp(-hdrColor * exposure); // - mapped = pow(mapped, vec3(1.0 / gamma)); FragColor = vec4(mapped, 1.0); } 

Nota por: agregue un gráfico para esta función con la exposición 1 y 2:


Aquí definimos una variable para la exposición, que es 1 por defecto y nos permite elegir con mayor precisión el equilibrio entre la calidad de visualización de las áreas oscuras y brillantes de la imagen. Por ejemplo, con una gran exposición, vemos muchos más detalles en las áreas oscuras de la imagen. Por el contrario, la baja exposición hace que las áreas oscuras no se puedan distinguir, pero le permite ver mejor las áreas brillantes de la imagen. A continuación se muestran imágenes de un túnel con diferentes niveles de exposición.



Estas imágenes muestran claramente los beneficios de la renderización hdr. A medida que cambia el nivel de exposición, vemos más detalles de la escena que se perderían en el renderizado normal. Tome el final del túnel como ejemplo: con una exposición normal, la textura del árbol es apenas visible, pero con una exposición baja, la textura es perfectamente visible. Del mismo modo, a alta exposición, los detalles en áreas oscuras son muy claramente visibles.


El código fuente de la demostración está aquí.


Más HDR


Esos dos algoritmos de compresión de tonos que se han mostrado son solo una pequeña parte entre una gran cantidad de algoritmos más avanzados, cada uno de los cuales tiene sus propias fortalezas y debilidades. Algunos algoritmos enfatizan mejor ciertos colores / brillos, algunos algoritmos muestran áreas oscuras y brillantes al mismo tiempo, dando imágenes más coloridas y detalladas. También hay muchos métodos conocidos como ajuste automático de exposición o adaptación ocular . Determinan el brillo de la escena en el cuadro anterior y (lentamente) cambian el parámetro de exposición, de modo que la escena oscura se vuelve más brillante y la más brillante: más oscura: similar a la habituación del ojo humano.


Los beneficios reales de HDR se ven mejor en escenas grandes y complejas con algoritmos de iluminación serios. Para fines de capacitación, este artículo utilizó la escena más simple posible, ya que crear una escena grande puede ser difícil. A pesar de la simplicidad de la escena, algunas ventajas de la renderización hdr son visibles: en las áreas oscuras y brillantes de la imagen, los detalles no se pierden, ya que se guardan mediante compresión de tonos, la adición de múltiples fuentes de luz no conduce a la aparición de áreas blancas, y los valores no tienen que encajar en LDR gama.


Además, el renderizado HDR también hace que algunos efectos interesantes sean más creíbles y realistas. Uno de estos efectos es la floración, que discutiremos en un artículo futuro.


Recursos adicionales



PD: Tenemos un telegrama conf para la coordinación de transferencias. Si tiene un serio deseo de ayudar con la traducción, ¡de nada!

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


All Articles