Aprende OpenGL. Lección 6.3 - Iluminación basada en imágenes. Irradiación difusa

OGL3 La iluminación basada en la imagen o IBL ( Image Based Lighting ) es una categoría de métodos de iluminación que no se basa en la contabilidad de fuentes de luz analíticas (discutidas en la lección anterior ), sino que considera el entorno completo de los objetos iluminados como una fuente de luz continua. En el caso general, la base técnica de tales métodos radica en el procesamiento de un mapa cúbico del entorno (preparado en el mundo real o creado sobre la base de una escena tridimensional) para que los datos almacenados en el mapa se puedan usar directamente en los cálculos de iluminación: de hecho, cada texto del mapa cúbico se considera como una fuente de luz . En general, esto le permite capturar el efecto de la iluminación global en la escena, que es un componente importante que transmite el "tono" general de la escena actual y ayuda a que los objetos iluminados se "incrusten" mejor en ella.

Dado que los algoritmos IBL tienen en cuenta la iluminación de un determinado entorno "global", su resultado se considera una simulación más precisa de la iluminación de fondo o incluso una aproximación muy aproximada de la iluminación global. Este aspecto hace que los métodos IBL sean interesantes en términos de incorporación al modelo PBR, ya que el uso de luz ambiental en el modelo de iluminación permite que los objetos se vean mucho más físicamente correctos.


Para incorporar la influencia de IBL en el sistema PBR ya descrito, volvemos a la ecuación de reflectancia familiar:

Lo(p, omegao)= int limits Omega(kd fracc pi+ks fracDFG4( omegao cdotn)( omegai cdotn))Li(p, omegai)n cdot omegaid omegai


Como se describió anteriormente, el objetivo principal es calcular la integral para todas las direcciones de radiación entrantes wi hemisferio  Omega . En la última lección, el cálculo de la integral no fue oneroso, ya que sabíamos de antemano el número de fuentes de luz y, por lo tanto, todas esas varias direcciones de incidencia de luz que les corresponden. Al mismo tiempo, la integral no se puede resolver con un chasquido: cualquier vector descendente wi desde el medio ambiente puede llevar brillo de energía no cero. Como resultado, para la aplicabilidad práctica del método es necesario cumplir los siguientes requisitos:
  • Debe encontrar una forma de obtener el brillo de energía de la escena para un vector de dirección arbitrario wi ;
  • Es necesario que la solución de la integral pueda ocurrir en tiempo real.

Bueno, el primer punto se resuelve por sí solo. Una pista de una solución ya se ha deslizado aquí: uno de los métodos para representar la irradiación de una escena o entorno es un mapa cúbico que ha sufrido un procesamiento especial. Cada texel en dicho mapa puede considerarse como una fuente emisora ​​separada. Al tomar muestras de dicho mapa de acuerdo con un vector arbitrario wi fácilmente obtenemos el brillo energético de la escena en esta dirección.

Entonces, obtenemos el brillo de energía de la escena para un vector arbitrario wi :

vec3 radiance = texture(_cubemapEnvironment, w_i).rgb; 

Sin embargo, notablemente, resolver la integral requiere que seleccionemos del mapa del entorno no desde una dirección, sino de todas las posibles en el hemisferio. Y así, para cada fragmento sombreado. Obviamente, para tareas en tiempo real esto es prácticamente impracticable. Un método más efectivo sería calcular parte de las operaciones integradas por adelantado, incluso fuera de nuestra aplicación. Pero para esto tendrás que arremangarte y sumergirte más profundamente en la esencia de la expresión de la reflectividad:

Lo(p, omegao)= int limits Omega(kd fracc pi+ks fracDFG4( omegao cdotn)( omegai cdotn))Li(p, omegai)n cdot omegaid omegai



Se puede ver que las partes de la expresión relacionadas con lo difuso kd y espejo ks Los componentes BRDF son independientes. Puede dividir la integral en dos partes:

Lo(p, omegao)= int limits Omega(kd fracc pi)Li(p, omegai)n cdot omegaid omegai+ int limits Omega(ks fracDFG4( omegao cdotn)( omegai cdotn))Li(p, omegai)n cdot omegaid omegai



Tal división en partes nos permitirá tratar con cada uno de ellos individualmente, y en esta lección trataremos con la parte responsable de la iluminación difusa.

Habiendo analizado la forma de la integral sobre el componente difuso, podemos concluir que el componente difuso de Lambert es esencialmente una constante (color s índice de refracción kd y  pi son constantes bajo las condiciones de integrando) y no depende de otras variables. Dado este hecho, podemos poner las constantes más allá del signo de la integral:

Lo(p, omegao)=kd fracc pi int limits OmegaLi(p, omegai)n cdot omegaid omegai



Entonces obtenemos una integral dependiendo solo de wi (se supone que p corresponde al centro del mapa cúbico del entorno). Según esta fórmula, puede calcular o, mejor aún, precalcular un nuevo mapa cúbico que almacena el resultado del cálculo de la integral del componente difuso para cada dirección de la muestra (o mapa de texel) wo utilizando la operación de convolución.

La convolución es la operación de aplicar algunos cálculos a cada elemento de un conjunto de datos, teniendo en cuenta los datos de todos los demás elementos del conjunto. En este caso, dichos datos son el brillo energético de la escena o el mapa del entorno. Por lo tanto, para calcular un valor en cada dirección de la muestra en el mapa cúbico, tendremos que tener en cuenta los valores tomados de todas las demás direcciones posibles de la muestra en el hemisferio que se encuentran alrededor del punto de muestra.

Para convolucionar el mapa del entorno, debe resolver la integral para cada dirección resultante de la muestra wo realizando múltiples muestras discretas a lo largo de las instrucciones wi perteneciente al hemisferio  Omega , y promediando el brillo de energía total. El hemisferio, en base al cual se toman las direcciones de muestreo wi orientado a lo largo del vector wo representando la dirección de destino para la cual se calcula la convolución actual. Mire la imagen para una mejor comprensión:



Tal mapa cúbico precalculado que almacena el resultado de integración para cada dirección de la muestra wo también se puede considerar como el almacenamiento del resultado de resumir toda la iluminación indirecta difusa en la escena, incidente en una determinada superficie orientada a lo largo de la dirección wo . En otras palabras, estos mapas cúbicos se denominan mapas de irradiancia, porque el mapa del entorno cúbico preconvolucional le permite muestrear directamente la magnitud de la irradiación de la escena, proveniente de una dirección arbitraria wo , sin cálculos adicionales.
La expresión que determina el brillo de la energía también depende de la posición del punto de muestreo. p que tomamos justo en el centro del mapa de irradiación. Esta suposición impone una limitación en el sentido de que la fuente de toda la iluminación indirecta difusa también será un único mapa ambiental. En escenas que son heterogéneas en iluminación, esto puede destruir la ilusión de la realidad (especialmente en escenas interiores). Los motores de renderizado modernos resuelven este problema colocando objetos auxiliares especiales en la escena: sondas de reflexión . Cada uno de estos objetos se dedica a una tarea: forma su propio mapa de irradiación para su entorno inmediato. Con esta técnica, la irradiación (y el brillo de la energía) en un punto arbitrario p se determinará por simple interpolación entre las muestras de reflexión más cercanas. Pero para las tareas actuales, estamos de acuerdo en que el mapa del entorno se muestrea desde su centro y analizaremos muestras de reflexión en lecciones adicionales.
A continuación se muestra un ejemplo de un mapa cúbico del entorno y un mapa de irradiación (basado en el motor de onda ) derivado del mismo, que promedia el brillo de la energía del entorno para cada dirección de salida wo .

Entonces, esta tarjeta almacena el resultado de convolución en cada texel (correspondiente a la dirección wo ), y externamente, dicho mapa parece almacenar el color promedio del mapa del entorno. Una muestra en cualquier dirección de dicho mapa devolverá el valor de la irradiación que emana de esta dirección.

PBR y HDR


En la lección anterior , ya se señaló brevemente que para el correcto funcionamiento del modelo de iluminación PBR es extremadamente importante tener en cuenta el rango de brillo HDR de las fuentes de luz presentes. Dado que el modelo PBR en la entrada toma parámetros de una forma u otra en función de cantidades y características físicas muy específicas, es lógico exigir que el brillo de energía de las fuentes de luz coincida con sus prototipos reales. No importa cómo justifiquemos el valor específico del flujo de radiación para cada fuente: hacemos una estimación de ingeniería aproximada o recurrimos a cantidades físicas ; la diferencia en las características entre una lámpara de habitación y el sol será enorme en cualquier caso. Sin el uso del rango HDR , simplemente será imposible determinar con precisión el brillo relativo de una variedad de fuentes de luz.

Entonces, PBR y HDR son amigos para siempre, esto es comprensible, pero ¿cómo se relaciona este hecho con los métodos de iluminación basados ​​en imágenes? En la última lección, se demostró que convertir PBR al rango de renderizado HDR es fácil. Queda un "pero": dado que la iluminación indirecta del entorno se basa en un mapa cúbico del entorno, se necesita una forma de preservar las características HDR de esta iluminación de fondo en el mapa del entorno.

Hasta ahora, hemos utilizado mapas de entorno creados en formato LDR (como los skyboxes ). Usamos la muestra de color de ellos en la representación tal cual y esto es bastante aceptable para el sombreado directo de los objetos. Y es completamente inadecuado cuando se usan mapas del entorno como fuentes de mediciones físicamente confiables.

RGBE: formato de imagen HDR


Familiarícese con el formato de archivo de imagen RGBE. Los archivos con la extensión " .hdr " se utilizan para almacenar imágenes con un amplio rango dinámico, asignando un byte para cada elemento de la tríada de color y un byte más para el exponente común. El formato también le permite almacenar mapas de entornos cúbicos con un rango de intensidad de color más allá del rango LDR [0., 1.]. Esto significa que las fuentes de luz pueden mantener su intensidad real, estando representadas por dicho mapa del entorno.

La red tiene muchos mapas de entorno gratuitos en formato RGBE, filmados en varias condiciones reales. Aquí hay un ejemplo del sitio de archivo sIBL :


Es posible que se sorprenda de lo que vio: después de todo, esta imagen distorsionada no se parece en nada a un mapa cúbico normal con su desglose pronunciado en 6 caras. La explicación es simple: este mapa del entorno se proyectó desde una esfera a un plano; se aplicó una exploración de igual rectángulo . Esto se hace para poder almacenar en un formato que no admite el modo de almacenamiento de tarjetas cúbicas tal como está. Por supuesto, este método de proyección tiene sus inconvenientes: la resolución horizontal es mucho más alta que la vertical. En la mayoría de los casos de aplicación en renderizado, esta es una relación aceptable, ya que generalmente los detalles interesantes del entorno y la iluminación se encuentran exactamente en el plano horizontal, y no en el vertical. Bueno, además de todo, necesitamos que el código de conversión regrese al mapa cúbico.

Soporte para formato RGBE en stb_image.h


La descarga de este formato de imagen por su cuenta requiere el conocimiento de la especificación del formato , que no es difícil, pero sigue siendo laborioso. Afortunadamente para nosotros , la biblioteca de carga de imágenes stb_image.h , implementada en un solo archivo de encabezado, admite la carga de archivos RGBE, devolviendo una matriz de números de punto flotante: ¡lo que necesitamos para nuestros propósitos! Agregar una biblioteca a su proyecto, cargar datos de imagen es extremadamente simple:

 #include "stb_image.h" [...] stbi_set_flip_vertically_on_load(true); int width, height, nrComponents; float *data = stbi_loadf("newport_loft.hdr", &width, &height, &nrComponents, 0); unsigned int hdrTexture; if (data) { glGenTextures(1, &hdrTexture); glBindTexture(GL_TEXTURE_2D, hdrTexture); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, width, height, 0, GL_RGB, GL_FLOAT, data); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); stbi_image_free(data); } else { std::cout << "Failed to load HDR image." << std::endl; } 

La biblioteca convierte automáticamente los valores del formato HDR interno a números reales reales de 32 bits, con tres canales de color por defecto. Es suficiente guardar los datos de la imagen HDR original en una textura de punto flotante 2D normal.

Convierta un escaneo de ángulo igual en un mapa cúbico


Se puede usar un escaneo igualmente rectangular para seleccionar directamente muestras del mapa del entorno, sin embargo, esto requeriría costosas operaciones matemáticas, mientras que la obtención de un mapa cúbico normal sería prácticamente libre de rendimiento. Es a partir de estas consideraciones que en esta lección trataremos la conversión de una imagen equilátera en un mapa cúbico, que se utilizará más adelante. Sin embargo, el método de muestreo directo de un mapa igualmente rectangular que usa un vector tridimensional también se mostrará aquí, para que pueda elegir el método de trabajo que más le convenga.

Para convertir, debe dibujar un cubo del tamaño de una unidad, observándolo desde el interior, proyectar un mapa rectangular igual en sus caras y luego extraer seis imágenes de las caras como caras del mapa cúbico. El sombreador de vértices de esta etapa es bastante simple: simplemente procesa los vértices del cubo tal como está y también pasa sus posiciones sin reformar al sombreador de fragmentos para usarlo como un vector de muestra tridimensional:

 #version 330 core layout (location = 0) in vec3 aPos; out vec3 localPos; uniform mat4 projection; uniform mat4 view; void main() { localPos = aPos; gl_Position = projection * view * vec4(localPos, 1.0); } 

En el sombreador de fragmentos, sombreamos cada cara del cubo como si estuviéramos tratando de envolver suavemente el cubo con una hoja con un mapa igualmente rectangular. Para hacer esto, se toma la dirección de la muestra transferida al sombreador de fragmentos, se procesa mediante magia trigonométrica especial y, en última instancia, la selección se realiza a partir de un mapa rectangular igual como si realmente fuera un mapa cúbico. El resultado de la selección se guarda directamente como el color del fragmento de la cara del cubo:

 #version 330 core out vec4 FragColor; in vec3 localPos; uniform sampler2D equirectangularMap; const vec2 invAtan = vec2(0.1591, 0.3183); vec2 SampleSphericalMap(vec3 v) { vec2 uv = vec2(atan(vz, vx), asin(vy)); uv *= invAtan; uv += 0.5; return uv; } void main() { // localPos   vec2 uv = SampleSphericalMap(normalize(localPos)); vec3 color = texture(equirectangularMap, uv).rgb; FragColor = vec4(color, 1.0); } 

Si realmente dibuja un cubo con este sombreador y un mapa de entorno HDR asociado, obtendrá algo como esto:


Es decir Se puede ver que, de hecho, proyectamos una textura rectangular en un cubo. Genial, pero ¿cómo nos ayudará esto a crear un mapa cúbico real? Para finalizar esta tarea, es necesario renderizar el mismo cubo 6 veces con una cámara mirando cada una de las caras, mientras escribe la salida en un objeto de búfer de cuadro separado:

 unsigned int captureFBO, captureRBO; glGenFramebuffers(1, &captureFBO); glGenRenderbuffers(1, &captureRBO); glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); glBindRenderbuffer(GL_RENDERBUFFER, captureRBO); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 512, 512); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, captureRBO); 

Por supuesto, no olvidaremos organizar la memoria para almacenar cada una de las seis caras del futuro mapa cúbico:

 unsigned int envCubemap; glGenTextures(1, &envCubemap); glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap); for (unsigned int i = 0; i < 6; ++i) { //  ,     // 16     glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, 512, 512, 0, GL_RGB, GL_FLOAT, nullptr); } glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 

Después de esta preparación, solo queda llevar a cabo la transferencia de partes de un mapa rectangular igual directamente al mapa cúbico.

No entraremos en demasiados detalles, especialmente porque el código se repite mucho visto en las lecciones sobre el búfer de cuadros y las sombras omnidireccionales . En principio, todo se reduce a preparar seis matrices de vista separadas que orientan la cámara estrictamente a cada una de las caras del cubo, así como una matriz de proyección especial con un ángulo de visión de 90 ° para capturar toda la cara del cubo. Luego, solo seis veces, se realiza el renderizado y el resultado se guarda en un framebuffer de punto flotante:

 glm::mat4 captureProjection = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, 10.0f); glm::mat4 captureViews[] = { glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f)), glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(-1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f)), glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, 1.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)), glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, -1.0f, 0.0f), glm::vec3(0.0f, 0.0f, -1.0f)), glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, 0.0f, 1.0f), glm::vec3(0.0f, -1.0f, 0.0f)), glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, 0.0f, -1.0f), glm::vec3(0.0f, -1.0f, 0.0f)) }; //  HDR        equirectangularToCubemapShader.use(); equirectangularToCubemapShader.setInt("equirectangularMap", 0); equirectangularToCubemapShader.setMat4("projection", captureProjection); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, hdrTexture); //         glViewport(0, 0, 512, 512); glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); for (unsigned int i = 0; i < 6; ++i) { equirectangularToCubemapShader.setMat4("view", captureViews[i]); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, envCubemap, 0); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); renderCube(); //    } glBindFramebuffer(GL_FRAMEBUFFER, 0); 

Aquí usamos el archivo adjunto del color del búfer de marco y cambiamos alternativamente la cara conectada del mapa cúbico, lo que conduce a la salida directa del renderizado a una de las caras del mapa del entorno. Este código debe ejecutarse solo una vez, después de lo cual aún tendremos un mapa de entorno envCubemap completo que contiene el resultado de convertir la versión original de formato rectangular igual del mapa de entorno HDR.

Probamos el mapa cúbico resultante al dibujar el sombreador de skybox más simple:

 #version 330 core layout (location = 0) in vec3 aPos; uniform mat4 projection; uniform mat4 view; out vec3 localPos; void main() { localPos = aPos; //         mat4 rotView = mat4(mat3(view)); vec4 clipPos = projection * rotView * vec4(localPos, 1.0); gl_Position = clipPos.xyww; } 

Preste atención al truco con los componentes del vector clipPos : usamos la tétrada xyww cuando registramos la coordenada transformada del vértice para asegurar que todos los fragmentos del skybox tengan una profundidad máxima de 1.0 (el enfoque ya se usó en la lección correspondiente ). No olvide cambiar la función de comparación a GL_LEQUAL :

 glDepthFunc(GL_LEQUAL); 

El sombreador de fragmentos simplemente selecciona de un mapa cúbico:

 #version 330 core out vec4 FragColor; in vec3 localPos; uniform samplerCube environmentMap; void main() { vec3 envColor = texture(environmentMap, localPos).rgb; envColor = envColor / (envColor + vec3(1.0)); envColor = pow(envColor, vec3(1.0/2.2)); FragColor = vec4(envColor, 1.0); } 

La selección del mapa se basa en las coordenadas locales interpoladas de los vértices del cubo, que es la dirección correcta de la selección en este caso (nuevamente, discutido en la lección sobre skyboxes, aprox. Por. ). Dado que los componentes de transporte en la matriz de vista fueron ignorados, el renderizado del skybox no dependerá de la posición del observador, creando la ilusión de un fondo infinitamente distante. Dado que aquí enviamos datos directamente desde la tarjeta HDR al framebuffer predeterminado, que es el receptor LDR, es necesario recuperar la compresión tonal. Y finalmente, casi todas las tarjetas HDR se almacenan en un espacio lineal, lo que significa que la corrección gamma debe aplicarse como el acorde de procesamiento final.

Entonces, al generar el skybox obtenido, junto con la ya familiar matriz de esferas, se obtiene algo similar:


Bueno, se gastó mucho esfuerzo, pero al final nos acostumbramos con éxito a leer el mapa del entorno HDR, convertirlo de un mapa equilátero a un mapa cúbico y generar el mapa cúbico HDR como un cuadro de cielo en la escena. Además, el código para convertir a un mapa cúbico mediante la representación en seis caras de un mapa cúbico es útil para nosotros más adelante en la tarea de convolución de un mapa de entorno . El código para todo el proceso de conversión está aquí .

Convolución de una carta cúbica


Como se dijo al comienzo de la lección, nuestro objetivo principal es resolver la integral para todas las direcciones posibles de iluminación difusa indirecta, teniendo en cuenta la irradiación dada de la escena en forma de un mapa cúbico del entorno. Se sabe que podemos obtener el valor del brillo energético de la escena. L(p,wi) para dirección arbitraria wi al tomar muestras del HDR de un mapa cúbico del entorno en esa dirección. Para resolver la integral, será necesario tomar muestras del brillo energético de la escena desde todas las direcciones posibles en el hemisferio.  Omega cada fragmento revisado
Obviamente, la tarea de muestrear la iluminación del ambiente desde todas las direcciones posibles en el hemisferio  Omega es computacionalmente imposible: hay un número infinito de tales direcciones. Sin embargo, es posible aplicar la aproximación tomando un número finito de direcciones elegidas al azar o ubicadas uniformemente dentro del hemisferio.Esto nos permitirá obtener una aproximación bastante buena a la irradiación verdadera, esencialmente resolviendo la integral que nos interesa en forma de suma finita.

Pero para las tareas en tiempo real, incluso este enfoque sigue siendo increíblemente impuesto, porque las muestras se toman para cada fragmento y el número de muestras debe ser lo suficientemente alto para obtener un resultado aceptable. Por lo tanto, sería bueno preparar de antemano los datos para este paso, fuera del proceso de representación. Dado que la orientación del hemisferio determina desde qué región del espacio capturamos la irradiación, es posible calcular de antemano la irradiación para cada posible orientación del hemisferio en función de todas las direcciones salientes posiblesw o :

L o ( p , ω o ) = k d cπΩLi(p,ωi)nωidωi



Como resultado, para un vector arbitrario dado w i , podemos tomar muestras del mapa de irradiancia calculado para obtener la irradiancia difusa en esta dirección. Para determinar la magnitud de la radiación difusa indirecta en el punto del fragmento actual, tomamos la irradiación total desde un hemisferio orientado a lo largo de la superficie normal al fragmento. En otras palabras, obtener la irradiación de una escena se reduce a una simple selección:

  vec3 irradiance = texture(irradianceMap, N); 

Además, para crear un mapa de irradiación, es necesario convolver el mapa del entorno, convertido en un mapa cúbico. Sabemos que para cada fragmento su hemisferio se considera orientado a lo largo de lo normal a la superficieN . En este caso, la convolución del mapa cúbico se reduce al cálculo de la cantidad promedio de brillo de energía desde todas las direcciones. w i dentro del hemisferioΩ orientado a lo largo de lo normalN :


Afortunadamente, el trabajo preliminar que llevó mucho tiempo que hicimos al comienzo de la lección ahora hará que sea bastante fácil convertir el mapa del entorno en un mapa cúbico en un sombreador de fragmentos especial, cuya salida se utilizará para formar un nuevo mapa cúbico. Para esto, es útil el mismo fragmento de código que se usó para traducir un mapa de entorno rectangular igual en un mapa cúbico.

Solo queda tomar otro sombreador de procesamiento:

 #version 330 core out vec4 FragColor; in vec3 localPos; uniform samplerCube environmentMap; const float PI = 3.14159265359; void main() { //       vec3 normal = normalize(localPos); vec3 irradiance = vec3(0.0); [...] //   FragColor = vec4(irradiance, 1.0); } 

Aquí, el sampler environmentMap es un mapa cúbico HDR del entorno previamente derivado de un equilátero.

Hay muchas formas de convolucionar el mapa del entorno. En este caso, para cada texel del mapa cúbico, generaremos varios vectores de muestra del hemisferioΩ , orientado a lo largo de la dirección de la muestra, y promediar los resultados. El número de vectores de muestra será fijo, y los propios vectores se distribuirán uniformemente dentro del hemisferio. Observo que el integrando es una función continua, y una estimación discreta de esta función será solo una aproximación. Y cuantos más vectores de muestreo tomemos, más cerca estaremos de la solución analítica de la integral. El integrando de la expresión para reflectividad depende del ángulo sólido

d w : valores con los que no es muy conveniente trabajar. En lugar de integrarse sobre un ángulo sólidod w cambiamos la expresión, lo que lleva a la integración sobre coordenadas esféricasθ y ϕ :


El ángulo Phi representará el acimut en el plano de la base del hemisferio, variando de 0 a 2 π . Ángulo θ representará el ángulo de elevación, que varía de 0 a12 π . La expresión modificada para la reflectividad en tales términos es la siguiente:

L o ( p , ϕ o , θ o ) = k d cπ 2 π ϕ = 0 12 πθ=0Li(p,ϕi,θi)cos(θ)sin(θ)dϕdθ



La solución de tal integral requerirá tomar un número finito de muestras en el hemisferio Ω y promediando los resultados. Saber la cantidad de muestrasn 1 y n 2 para cada una de las coordenadas esféricas, podemos traducir la integral a lasuma riemanniana:

L o ( p , ϕ o , θ o ) = k d cπ 1n 1 n 2 n 1 ϕ=0 n 2 θ=0Li(p,ϕi,θi)cos(θ)sin(θ)dϕdθ


Dado que ambas coordenadas esféricas varían discretamente, en cada momento, el muestreo se realiza con un área promedio determinada en el hemisferio, como se puede ver en la figura anterior. Debido a la naturaleza de la superficie esférica, el tamaño del área de muestreo discreto disminuye inevitablemente al aumentar el ángulo de elevaciónθ y acercándose al cenit. Para compensar este efecto de reducir el área, agregamos un coeficiente de peso a la expresións i n θ .

Como resultado, la implementación del muestreo discreto en el hemisferio basado en coordenadas esféricas para cada fragmento en forma de código es la siguiente:

 vec3 irradiance = vec3(0.0); vec3 up = vec3(0.0, 1.0, 0.0); vec3 right = cross(up, normal); up = cross(normal, right); float sampleDelta = 0.025; float nrSamples = 0.0; for(float phi = 0.0; phi < 2.0 * PI; phi += sampleDelta) { for(float theta = 0.0; theta < 0.5 * PI; theta += sampleDelta) { //   .   (  -) vec3 tangentSample = vec3(sin(theta) * cos(phi), sin(theta) * sin(phi), cos(theta)); //      vec3 sampleVec = tangentSample.x * right + tangentSample.y * up + tangentSample.z * N; irradiance += texture(environmentMap, sampleVec).rgb * cos(theta) * sin(theta); nrSamples++; } } irradiance = PI * irradiance * (1.0 / float(nrSamples)); 

La variable sampleDelta determina el tamaño del paso discreto a lo largo de la superficie del hemisferio. Al cambiar este valor, puede aumentar o disminuir la precisión del resultado.

Dentro de ambos ciclos, se forma un vector de muestra tridimensional regular a partir de coordenadas esféricas, se transfiere de la tangente al espacio mundial y luego se usa para muestrear un mapa de entorno cúbico desde el HDR. El resultado de las muestras se acumula en la variable de irradiancia , que al final del procesamiento se dividirá por el número de muestras realizadas para obtener un valor promedio de irradiación. Tenga en cuenta que el resultado del muestreo de la textura se modula en dos cantidades: cos (theta) : para tener en cuenta la atenuación de la luz en ángulos grandes y sin (theta)- para compensar la reducción en el área de muestra al acercarse al cenit.

Solo queda tratar con el código que representa y captura los resultados de la convolución del mapa del entorno envCubemap . Primero, cree un mapa cúbico para almacenar la irradiación (deberá hacerlo una vez, antes de ingresar al ciclo de renderizado principal):

 unsigned int irradianceMap; glGenTextures(1, &irradianceMap); glBindTexture(GL_TEXTURE_CUBE_MAP, irradianceMap); for (unsigned int i = 0; i < 6; ++i) { glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, 32, 32, 0, GL_RGB, GL_FLOAT, nullptr); } glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 

Dado que el mapa de irradiación se obtiene promediando muestras distribuidas uniformemente del brillo de la energía del mapa ambiental, prácticamente no contiene partes y elementos de alta frecuencia; una textura de resolución bastante baja (32x32 aquí) y un filtrado lineal habilitado serán suficientes para almacenarlo.

A continuación, configure el framebuffer de captura a esta resolución:

 glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); glBindRenderbuffer(GL_RENDERBUFFER, captureRBO); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 32, 32); 

El código para capturar los resultados de convolución es similar al código para transferir un mapa de entorno de uno equilátero a uno cúbico, solo se usa un sombreador de convolución:

 irradianceShader.use(); irradianceShader.setInt("environmentMap", 0); irradianceShader.setMat4("projection", captureProjection); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap); //        glViewport(0, 0, 32, 32); glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); for (unsigned int i = 0; i < 6; ++i) { irradianceShader.setMat4("view", captureViews[i]); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, irradianceMap, 0); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); renderCube(); } glBindFramebuffer(GL_FRAMEBUFFER, 0); 

Después de completar esta etapa, tendremos un mapa de irradiación precalculado en nuestras manos que se puede usar directamente para calcular la iluminación difusa indirecta. Para verificar cómo fue la convolución, intentaremos reemplazar la textura del skybox del mapa del entorno con el mapa de irradiación:


Si, como resultado, viste algo que parecía un mapa muy borroso del entorno, entonces, muy probablemente, la convolución fue exitosa.

PBR e iluminación indirecta


El mapa de irradiación resultante se usa en la parte difusa de la expresión dividida de reflectividad y representa la contribución acumulada de todas las direcciones posibles de iluminación indirecta. Dado que en este caso la luz no proviene de fuentes específicas, sino del entorno en su conjunto, consideramos la iluminación indirecta difusa y espejo como fondo ( ambiente ), reemplazando el valor constante utilizado anteriormente.

Para empezar, no olvide agregar una nueva muestra con un mapa de irradiación:

 uniform samplerCube irradianceMap; 

Tener un mapa de irradiación que almacena toda la información sobre la radiación difusa indirecta desde la escena y la normal a la superficie, obtener datos sobre la irradiación de un fragmento en particular es tan simple como hacer una muestra de la textura:

 // vec3 ambient = vec3(0.03); vec3 ambient = texture(irradianceMap, N).rgb; 

Sin embargo, dado que la radiación indirecta contiene datos para los componentes difusos y espejo (como vimos en la versión componente de la expresión de reflectividad), necesitamos modular el componente difuso de una manera especial. Como en la lección anterior, usamos la expresión de Fresnel para determinar el grado de reflexión de la luz para una superficie dada, de donde obtenemos el grado de refracción de la luz o el coeficiente difuso:

 vec3 kS = fresnelSchlick(max(dot(N, V), 0.0), F0); vec3 kD = 1.0 - kS; vec3 irradiance = texture(irradianceMap, N).rgb; vec3 diffuse = irradiance * albedo; vec3 ambient = (kD * diffuse) * ao; 

A medida que la iluminación de fondo cae desde todas las direcciones en el hemisferio en función de la superficie normal N , es imposible determinar la única mediana (amitad de camino) vector para calcular el coeficiente de Fresnel. Para simular el efecto Fresnel en tales condiciones, es necesario calcular el coeficiente basado en el ángulo entre el vector normal y el vector de observación. Sin embargo, anteriormente, como parámetro para calcular el coeficiente de Fresnel, utilizamos el vector mediano obtenido en base al modelo de micro superficies y dependiendo de la rugosidad de la superficie. Como en este caso, la rugosidad no se incluye en los parámetros de cálculo, el grado de reflexión de la luz por la superficie siempre se sobreestimará. La iluminación indirecta en su conjunto debe comportarse igual que la iluminación directa, es decir. de superficies rugosas esperamos un menor grado de reflexión en los bordes. Pero como la aspereza no se tiene en cuenta,entonces el grado de reflexión especular según Fresnel para la iluminación indirecta parece poco realista en superficies rugosas no metálicas (en la imagen a continuación, el efecto descrito se exagera para una mayor claridad):


Puede evitar esta molestia introduciendo aspereza en la expresión de Fremlin-Schlick, un proceso descrito por Sébastien Lagarde :

 vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness) { return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0); } 

Dada la rugosidad de la superficie al calcular el conjunto de Fresnel, el código para calcular el componente de fondo toma la siguiente forma:

 vec3 kS = fresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness); vec3 kD = 1.0 - kS; vec3 irradiance = texture(irradianceMap, N).rgb; vec3 diffuse = irradiance * albedo; vec3 ambient = (kD * diffuse) * ao; 

Al final resultó que, el uso de iluminación basada en imágenes se reduce inherentemente a una muestra de un mapa cúbico. Todas las dificultades se asocian principalmente con la preparación preliminar y la transferencia del mapa ambiental al mapa de irradiación.

Tomando una escena familiar de una lección sobre fuentes de luz analíticas que contienen una variedad de esferas con diferente metalicidad y aspereza, y agregando iluminación de fondo difusa del ambiente, obtienes algo como esto:


Todavía parece extraño, ya que los materiales con un alto grado de metalicidad aún requieren reflexión para verse realmente, hmm, metal (los metales no reflejan una iluminación difusa, después de todo). Y en este caso, las únicas reflexiones obtenidas de fuentes de luz analíticas puntuales. Y, sin embargo, ya podemos decir que las esferas se ven más inmersas en el entorno (especialmente notable al cambiar los mapas del entorno), ya que las superficies ahora responden correctamente a la iluminación de fondo del entorno de la escena.

El código fuente completo de la lección está aquí.. En la próxima lección, finalmente abordaremos la segunda mitad de la expresión de reflectividad, que es responsable de la iluminación especular indirecta. Después de este paso, realmente sentirá el poder del enfoque PBR en la iluminación.

Materiales 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/es426987/


All Articles