
En la
lección anterior, preparamos nuestro modelo PBR para trabajar con el método IBL; para ello, teníamos que preparar un mapa de irradiación por adelantado que describiera la parte difusa de la iluminación indirecta. En esta lección prestaremos atención a la segunda parte de la expresión de la reflectividad: el espejo:
Lo(p, omegao)= int limits Omega(kd fracc pi+ks fracDFG4( omegao cdotn)( omegai cdotn))Li(p, omegai)n cdot omegaid omegai
Puede notar que el componente espejo Cook-Torrens (subexpresión con un factor
ks ) no es constante y depende de la dirección de la luz incidente,
así como de la dirección de observación. La solución de esta integral para todas las direcciones posibles de incidencia de luz, junto con todas las direcciones posibles de observación en tiempo real, simplemente no es factible. Por lo tanto, los investigadores de Epic Games han propuesto un enfoque llamado
aproximación de suma dividida , que le permite preparar parcialmente los datos para el componente espejo de antemano, sujeto a ciertas condiciones.
En este enfoque, el componente espejo de la expresión de reflectancia se divide en dos partes, que pueden ser preconvolucionadas individualmente y luego combinadas en un sombreador PBR para ser utilizadas como fuente de radiación especular indirecta. Al igual que con la generación del mapa de irradiación, el proceso de convolución recibe un mapa del entorno HDR en su entrada.
Para comprender el método de aproximación de suma dividida, veamos nuevamente la expresión de reflectividad, dejando solo la subexpresión para el componente espejo (la parte difusa se consideró por separado en la
lección anterior ):
Lo(p, omegao)= int limits Omega(ks fracDFG4( omegao cdotn)( omegai cdotn)Li(p, omegai)n cdot omegaid omegai= int limits Omegafr(p, omegai, omegao)Li(p, omegai)n cdot omegaid omegai
Al igual que con la preparación del mapa de irradiación, esta integral no es posible de resolver en tiempo real. Por lo tanto, es aconsejable calcular de manera similar el mapa para el componente espejo de la expresión de reflectividad, y en el ciclo de renderizado principal, haga una selección simple de este mapa en función de lo normal a la superficie. Sin embargo, no todo es tan simple: el mapa de irradiación se obtuvo con relativa facilidad debido al hecho de que la integral dependía solo de
omegai , y la subexpresión constante para el componente difuso lambertiano podría extraerse del signo de la integral. En este caso, la integral depende no solo de
omegai eso es fácil de entender de la fórmula BRDF:
fr(p,wi,wo)= fracDFG4( omegao cdotn)( omegai cdotn)
La expresión debajo de la integral también depende de
omegao - para vectores de dos direcciones, es casi imposible seleccionar de un mapa cúbico previamente preparado. Posición del punto
p en este caso, no puede tener en cuenta: por qué se discutió esto en la lección anterior. Cálculo preliminar de la integral para todas las combinaciones posibles.
omegai y
omegao imposible en tareas en tiempo real.
El método de cantidad dividida de Epic Games resuelve este problema dividiendo el problema de cálculo preliminar en dos partes independientes, cuyos resultados se pueden combinar más adelante para obtener el valor calculado final. El método de suma dividida extrae dos integrales de la expresión original para el componente espejo:
Lo(p, omegao)= int limits OmegaLi(p, omegai)d omegai∗ int limits Omegafr(p, omegai, omegao)n cdot omegaid omegai
El resultado del cálculo de la primera parte generalmente se denomina
mapa de entorno prefiltrado , y es un mapa de entorno sujeto al proceso de convolución especificado por esta expresión. Todo esto es similar al proceso de obtención de un mapa de irradiación, pero en este caso, la convolución se lleva a cabo teniendo en cuenta el valor de rugosidad. Los valores de rugosidad más altos conducen al uso de vectores de muestreo más dispares en el proceso de convolución, lo que resulta en resultados más borrosos. El resultado de convolución para cada siguiente nivel de rugosidad seleccionado se almacena en el siguiente nivel de mip del mapa de entorno preparado. Por ejemplo, un mapa de entorno, convolucionado para cinco niveles de rugosidad diferentes, contiene cinco niveles de mip y se ve más o menos así:
Los vectores de muestra y su propagación se determinan en función de la función de distribución normal (
NDF ) del modelo BRDF de Cook-Torrens. Esta función acepta el vector normal y la dirección de observación como parámetros de entrada. Dado que la dirección de observación no se conoce de antemano en el momento del cálculo preliminar, los desarrolladores de Epic Games tuvieron que hacer una suposición más: la dirección de la mirada (y, por lo tanto, la dirección de reflexión especular) siempre es idéntica a la dirección de salida de la muestra
omegao . En forma de código:
vec3 N = normalize(w_o); vec3 R = N; vec3 V = R;
En tales condiciones, la dirección de la mirada no es necesaria en el proceso de convolución del mapa del entorno, lo que hace que el cálculo sea factible en tiempo real. Pero, por otro lado, perdemos la distorsión característica de los reflejos especulares cuando se observa en un ángulo agudo con respecto a la superficie reflectante, como se puede ver en la imagen a continuación (desde
Moving Frostbite a PBR ). En general, tal compromiso se considera aceptable.
La segunda parte de la expresión de suma dividida contiene el BRDF de la expresión original para el componente espejo. Suponiendo que el brillo de la energía entrante se representa espectralmente con luz blanca para todas las direcciones (es decir,
L(p,x)=1.0 ), es posible calcular previamente el valor de BRDF con los siguientes parámetros de entrada: rugosidad del material y el ángulo entre el normal
n y dirección de la luz
omegai (o
n cdot omegai ) El enfoque de Epic Games implica almacenar los resultados del cálculo BRDF para cada combinación de rugosidad y el ángulo entre la normal y la dirección de la luz en forma de una textura bidimensional conocida como el
mapa de integración BRDF , que luego se utiliza como una
tabla de consulta (
LUT ) . Esta textura de referencia utiliza los canales de salida rojo y verde para almacenar la escala y el desplazamiento para calcular el coeficiente de Fresnel de la superficie, lo que finalmente nos permite resolver la segunda parte de la expresión para la suma separada:

Esta textura auxiliar se crea de la siguiente manera: las coordenadas de textura horizontales (que van desde [0., 1.]) se consideran valores de parámetros de entrada
n cdot omegai Funciones BRDF; Las coordenadas de textura vertical se consideran valores de rugosidad de entrada.
Como resultado, al tener un mapa de integración y un mapa de entorno preprocesados, puede combinar las muestras de ellos para obtener el valor final de la expresión integral del componente espejo:
float lod = getMipLevelFromRoughness(roughness); vec3 prefilteredColor = textureCubeLod(PrefilteredEnvMap, refVec, lod); vec2 envBRDF = texture2D(BRDFIntegrationMap, vec2(NdotV, roughness)).xy; vec3 indirectSpecular = prefilteredColor * (F * envBRDF.x + envBRDF.y)
Esta revisión del método de suma dividida de Epic Games debería ayudarlo a tener una idea del proceso de aproximación de la parte de la expresión de reflectancia que es responsable del componente espejo. Ahora intentemos preparar los datos de la tarjeta nosotros mismos.
Prefiltrado del mapa del entorno HDR
Prefiltrar el mapa del entorno es similar a lo que se hizo para obtener un mapa de irradiación. La única diferencia es que ahora tenemos en cuenta la rugosidad y guardamos el resultado para cada nivel de rugosidad en el nuevo nivel de mip del mapa cúbico.
Primero deberá crear un nuevo mapa cúbico que contendrá el resultado del prefiltrado. Para crear el número necesario de niveles de
mip , simplemente llamamos a
glGenerateMipmaps () : se asignará la memoria necesaria para la textura actual:
unsigned int prefilterMap; glGenTextures(1, &prefilterMap); glBindTexture(GL_TEXTURE_CUBE_MAP, prefilterMap); for (unsigned int i = 0; i < 6; ++i) { glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, 128, 128, 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_MIPMAP_LINEAR); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glGenerateMipmap(GL_TEXTURE_CUBE_MAP);
Tenga en cuenta: dado que la selección de
prefilterMap se basará en la existencia de niveles de mip, es necesario establecer el modo de filtro de reducción en
GL_LINEAR_MIPMAP_LINEAR para habilitar el filtrado trilineal. Las imágenes preprocesadas de imágenes especulares se almacenan en caras separadas del mapa cúbico con una resolución en el nivel base de mip de solo 128x128 píxeles. Para la mayoría de los materiales, esto es suficiente, sin embargo, si su escena tiene un mayor número de superficies lisas y brillantes (por ejemplo, un automóvil nuevo), es posible que deba aumentar esta resolución.
En la lección anterior, incluimos el mapa del entorno creando vectores de muestra que se distribuyen uniformemente en el hemisferio
Omega usando coordenadas esféricas. Para obtener la irradiación, este método es bastante efectivo, lo que no se puede decir sobre los cálculos de reflexiones especulares. La física de los reflejos especulares nos dice que la dirección de la luz reflejada especularmente es adyacente al vector de reflexión.
r para una superficie normal
n incluso si la rugosidad no es cero:
La forma generalizada de posibles direcciones salientes de reflexión se llama
lóbulo especular (
lóbulo especular ; "pétalo de un patrón de radiación espejo" - tal vez demasiado detallado,
aprox. Per. ). Con un aumento en la aspereza, el pétalo crece y se expande. Además, su forma cambia según la dirección de la incidencia de la luz. Por lo tanto, la forma del pétalo depende en gran medida de las propiedades del material.
Volviendo al modelo de las micro superficies, podemos imaginar la forma del lóbulo del espejo como una descripción de la orientación de la reflexión con respecto al vector mediano de las micro-superficies, teniendo en cuenta alguna dirección dada de incidencia de la luz. Teniendo en cuenta que la mayoría de los rayos de luz reflejada se encuentran dentro de un pétalo de espejo orientado sobre la base del vector mediano, tiene sentido crear vectores de muestra orientados de manera similar. De lo contrario, muchos de ellos serán inútiles. Este enfoque se llama
muestreo de importancia .
Integración de Monte Carlo y muestreo de significación
Para comprender completamente el significado de la muestra en términos de significación, primero deberá familiarizarse con un aparato matemático como el método de integración de Monte Carlo. Este método se basa en una combinación de estadística y teoría de la probabilidad y ayuda a resolver numéricamente un problema estadístico en una muestra grande sin la necesidad de considerar
cada elemento de esta muestra.
Por ejemplo, desea calcular el crecimiento promedio de la población de un país. Para obtener un resultado preciso y confiable, uno tendría que medir el crecimiento de
cada ciudadano y promediar el resultado. Sin embargo, dado que la población de la mayoría de los países es bastante grande, este enfoque es prácticamente irrealizable, ya que requiere demasiados recursos para su ejecución.
Otro enfoque es crear una submuestra más pequeña llena de elementos verdaderamente aleatorios (imparciales) de la muestra original. A continuación, también mide el crecimiento y promedia el resultado de esta submuestra. Puede tomar al menos un centenar de personas y obtener un resultado, aunque no del todo exacto, pero aún bastante cercano a la situación real. La explicación de este método radica en la consideración de la ley de los grandes números. Y su esencia se describe de esta manera: el resultado de alguna medición en una submuestra más pequeña
N , compuesto de elementos verdaderamente aleatorios del conjunto original, estará cerca del resultado de control de las mediciones tomadas en todo el conjunto inicial. Además, el resultado aproximado tiende a ser cierto con el crecimiento.
N .
La integración de Monte Carlo es la aplicación de la ley de los grandes números para resolver integrales. En lugar de resolver la integral, tener en cuenta todo el conjunto (posiblemente infinito) de valores
x nosotros usamos
N puntos de muestra aleatorios y promedie el resultado. Con crecimiento
N Se garantiza que el resultado aproximado se acercará a la solución exacta de la integral.
O= int limitsbaf(x)dx= frac1N sumN−1i=0 fracf(x)pdf(x)
Para resolver la integral, obtenemos el valor del integrando para
N puntos aleatorios de la muestra dentro de [a, b], los resultados se resumen y dividen por el número total de puntos tomados para promediar. Artículo
pdf describe
la función de densidad de probabilidad , que muestra la probabilidad con la que se produce cada valor seleccionado en la muestra original. Por ejemplo, esta función para el crecimiento de los ciudadanos se vería así:
Se puede ver que cuando se usan puntos de muestreo aleatorios, tenemos muchas más posibilidades de alcanzar un valor de crecimiento de 170 cm que alguien con un crecimiento de 150 cm.
Está claro que durante la integración de Monte Carlo, algunos puntos de muestra tienen más probabilidades de aparecer en la secuencia que otros. Por lo tanto, en cualquier expresión para la estimación de Monte Carlo, dividimos o multiplicamos el valor seleccionado por la probabilidad de que ocurra usando la función de densidad de probabilidad. Por el momento, al evaluar la integral, creamos muchos puntos de muestra distribuidos uniformemente: la posibilidad de obtener cualquiera de ellos era la misma. Por lo tanto, nuestra estimación fue
imparcial , lo que significa que a medida que aumenta el número de puntos de muestra, nuestra estimación convergerá a la solución exacta de la integral.
Sin embargo, hay funciones de evaluación que están
sesgadas , es decir implicando la creación de puntos de muestreo no de una manera verdaderamente aleatoria, sino con un predominio de cierta magnitud o dirección. Dichas funciones de evaluación permiten que la estimación de Monte Carlo converja a la solución exacta
mucho más rápido . Por otro lado, debido al sesgo de la función de evaluación, es posible que la solución nunca converja. En el caso general, esto se considera un compromiso aceptable, especialmente en problemas de gráficos por computadora, ya que la estimación es muy cercana al resultado analítico y no es necesaria si su efecto parece bastante confiable visualmente. Como pronto veremos la muestra por significancia (usando una función de estimación sesgada) le permite crear puntos de muestra sesgados hacia una determinada dirección, lo cual se tiene en cuenta al multiplicar o dividir cada valor seleccionado por el valor correspondiente de la función de densidad de probabilidad.
La integración de Monte Carlo es bastante común en problemas de gráficos de computadora, ya que es un método bastante intuitivo para estimar el valor de integrales continuas mediante un método numérico, que es bastante efectivo. Es suficiente tomar un área o volumen en el que se toma la muestra (por ejemplo, nuestro hemisferio
Omega ), crear
N puntos de muestreo aleatorio que se encuentran en el interior y realizan una suma ponderada de los valores obtenidos.
El método de Monte Carlo es un tema muy extenso de discusión, y aquí ya no entraremos en detalles, pero hay un detalle más importante: de ninguna manera hay una manera de crear
muestras aleatorias . Por defecto, cada punto de muestra es completamente (psvedo) aleatorio, que es lo que esperamos. Pero, usando ciertas propiedades de secuencias cuasialeatorias, es posible crear conjuntos de vectores que, aunque aleatorios, tienen propiedades interesantes. Por ejemplo, al crear muestras aleatorias para el proceso de integración, puede utilizar las llamadas
secuencias de baja discrepancia , que garantizan la aleatoriedad de los puntos de muestreo creados, pero en el conjunto general se
distribuyen de manera más uniforme:
El uso de secuencias de baja discrepancia para crear un conjunto de vectores de muestra para el proceso de integración es el
método de integración
Cuasi-Monte Carlo . Los cuasi-métodos de Monte Carlo convergen mucho más rápido que el enfoque general, que es una propiedad muy atractiva para aplicaciones con requisitos de alto rendimiento.
Entonces, sabemos sobre el método general y cuasi-Monte Carlo, pero hay un detalle más que proporcionará una tasa de convergencia aún mayor: una muestra por significación.
Como ya se señaló en la lección, para las reflexiones especulares, la dirección de la luz reflejada está encerrada en un lóbulo especular, cuyo tamaño y forma dependen de la rugosidad de la superficie reflectante. Entender que cualquier vector de muestra (cuasi) aleatorio que esté fuera del lóbulo espejo no afectará la expresión integral del componente espejo, es decir. inútil Tiene sentido enfocar la generación de vectores de muestra en la región del lóbulo espejo usando la función de estimación sesgada para el método de Monte Carlo.
Esta es la esencia del muestreo por importancia: la creación de vectores de muestreo está encerrada en un área determinada orientada a lo largo del vector mediano de micro-superficies, y cuya forma está determinada por la rugosidad del material. Usando una combinación del cuasi-método Monte Carlo, secuencias de baja discrepancia y sesgo en el proceso de creación de vectores de muestra debido al muestreo en importancia, logramos tasas de convergencia muy altas. Dado que la convergencia a la solución es lo suficientemente rápida, podemos usar un número menor de vectores de muestra para lograr una estimación suficientemente aceptable. La combinación de métodos descrita, en principio, permite que las aplicaciones gráficas incluso resuelvan la integral del componente espejo en tiempo real, aunque el cálculo preliminar sigue siendo un enfoque mucho más rentable.
Baja secuencia de desajuste
En esta lección, todavía usamos un cálculo preliminar del componente espejo de la expresión de reflectancia para la radiación indirecta. Y usaremos una muestra de significación utilizando una secuencia aleatoria de baja discrepancia y el cuasi-método de Monte Carlo. La secuencia utilizada se conoce como la
secuencia de Hammersley , una descripción detallada de la cual es dada por
Holger Dammertz . Esta secuencia, a su vez, se basa en la
secuencia de van der Corput , que utiliza una transformación binaria especial de la fracción decimal en relación con el punto decimal.
Usando trucos aritméticos bit a bit complicados, puede configurar de manera bastante eficiente la secuencia de van der Corpute directamente en el sombreador y, en función de ello, crear el elemento i-ésimo de la secuencia de Hammersley a partir de la selección en
N artículos:
float RadicalInverse_VdC(uint bits) { bits = (bits << 16u) | (bits >> 16u); bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); return float(bits) * 2.3283064365386963e-10; // / 0x100000000 } // ---------------------------------------------------------------------------- vec2 Hammersley(uint i, uint N) { return vec2(float(i)/float(N), RadicalInverse_VdC(i)); }
Hammersley () devuelve el elemento i-ésimo de una secuencia de baja coincidencia de muestras de varios tamaños
N .
No todos los controladores OpenGL admiten operaciones bit a bit (WebGL y OpenGL ES 2.0, por ejemplo), por lo que para ciertos entornos, puede ser necesaria una implementación alternativa de su uso:
float VanDerCorpus(uint n, uint base) { float invBase = 1.0 / float(base); float denom = 1.0; float result = 0.0; for(uint i = 0u; i < 32u; ++i) { if(n > 0u) { denom = mod(float(n), 2.0); result += denom * invBase; invBase = invBase / 2.0; n = uint(float(n) / 2.0); } } return result; } // ---------------------------------------------------------------------------- vec2 HammersleyNoBitOps(uint i, uint N) { return vec2(float(i)/float(N), VanDerCorpus(i, 2u)); }
Noto que debido a ciertas restricciones en los operadores de ciclo en el hardware antiguo, esta implementación pasa por los 32 bits. Como resultado, esta versión no es tan productiva como la primera opción, pero funciona en cualquier hardware, e incluso en ausencia de operaciones de bits.
Muestra de importancia en el modelo GGX
En lugar de una distribución uniforme o aleatoria (Monte Carlo) de los vectores de muestra generados dentro del hemisferio
Omega , que aparece en la integral que estamos resolviendo, intentaremos crear vectores para que graviten en la dirección principal de la reflexión de la luz, caracterizada por el vector mediano de las micro superficies y dependiendo de la rugosidad de la superficie. El proceso de muestreo en sí será similar al previamente considerado: abrir un ciclo con un número suficientemente grande de iteraciones, crear un elemento de una secuencia de baja coincidencia, en base a esto creamos un vector de muestreo en el espacio tangente, transferir este vector a las coordenadas mundiales y usar el brillo de energía de la escena para muestrear. En principio, los cambios se relacionan solo con el hecho de que ahora se utiliza un elemento de la secuencia de baja discrepancia para especificar un nuevo vector de muestra:
const uint SAMPLE_COUNT = 4096u; for(uint i = 0u; i < SAMPLE_COUNT; ++i) { vec2 Xi = Hammersley(i, SAMPLE_COUNT);
Además, para la formación completa del vector de muestra, será necesario orientarlo de alguna manera en la dirección del lóbulo espejo correspondiente a un determinado nivel de rugosidad. Puede tomar el NDF (función de distribución normal) de una
lección de teoría y combinarlo con el GGX NDF para el método de especificar un vector de muestra en una esfera de autoría Epic Games:
vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness) { float a = roughness*roughness; float phi = 2.0 * PI * Xi.x; float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a*a - 1.0) * Xi.y)); float sinTheta = sqrt(1.0 - cosTheta*cosTheta);
El resultado es un vector de muestra, orientado aproximadamente a lo largo del vector mediano de las micro superficies, para una rugosidad dada y un elemento de la secuencia de baja desadaptación
Xi . Tenga en cuenta que Epic Games utiliza el cuadrado del valor de rugosidad para una mayor calidad visual, basado en el trabajo original de Disney sobre el método PBR.
Una vez finalizada la implementación de la secuencia de Hammersley y el código de generación del vector de muestra, podemos proporcionar el código de sombreado de pre-filtrado y convolución:
#version 330 core out vec4 FragColor; in vec3 localPos; uniform samplerCube environmentMap; uniform float roughness; const float PI = 3.14159265359; float RadicalInverse_VdC(uint bits); vec2 Hammersley(uint i, uint N); vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness); void main() { vec3 N = normalize(localPos); vec3 R = N; vec3 V = R; const uint SAMPLE_COUNT = 1024u; float totalWeight = 0.0; vec3 prefilteredColor = vec3(0.0); for(uint i = 0u; i < SAMPLE_COUNT; ++i) { vec2 Xi = Hammersley(i, SAMPLE_COUNT); vec3 H = ImportanceSampleGGX(Xi, N, roughness); vec3 L = normalize(2.0 * dot(V, H) * H - V); float NdotL = max(dot(N, L), 0.0); if(NdotL > 0.0) { prefilteredColor += texture(environmentMap, L).rgb * NdotL; totalWeight += NdotL; } } prefilteredColor = prefilteredColor / totalWeight; FragColor = vec4(prefilteredColor, 1.0); }
Llevamos a cabo un filtrado preliminar del mapa del entorno en función de cierta rugosidad dada, cuyo nivel cambia para cada nivel de mip del mapa cúbico resultante (de 0.0 a 1.0), y el resultado del filtro se almacena en la variable prefilteredColor . A continuación, la variable se divide por el peso total de toda la muestra, y las muestras con una contribución menor al resultado final (que tiene un valor NdotL más bajo ) también aumentan el peso total menos.Guardar datos de prefiltrado en niveles mip
Queda por escribir el código que le indica directamente a OpenGL que filtre el mapa del entorno con varios niveles de rugosidad y luego guarde los resultados en una serie de niveles mip del mapa cúbico objetivo. Aquí, el código ya preparado de la lección sobre el cálculo del mapa de irradiación es útil : prefilterShader.use(); prefilterShader.setInt("environmentMap", 0); prefilterShader.setMat4("projection", captureProjection); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap); glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); unsigned int maxMipLevels = 5; for (unsigned int mip = 0; mip < maxMipLevels; ++mip) {
El proceso es similar a una convolución del mapa de irradiación, pero esta vez debe especificar el tamaño del búfer de trama en cada paso, reduciéndolo a la mitad para que coincida con los niveles de mip. Además, el nivel de mip al que se realizará el renderizado en este momento debe especificarse como el parámetro de la función glFramebufferTexture2D () .El resultado de la ejecución de este código debe ser un mapa cúbico que contenga imágenes de reflejos cada vez más borrosas en cada nivel de mip posterior. Puede usar un mapa cúbico como fuente de datos para skybox y tomar una muestra de cualquier nivel de mip por debajo de cero: vec3 envColor = textureLod(environmentMap, WorldPos, 1.2).rgb;
El resultado de esta acción será la siguiente imagen:Parece un mapa de entorno fuente muy borroso. Si su resultado es similar, lo más probable es que el proceso de filtrado preliminar del mapa del entorno HDR se realice correctamente. Intente experimentar con una muestra de diferentes niveles de mip y observe un aumento gradual en el desenfoque con cada nivel siguiente.Artefactos de convolución de prefiltro
Para la mayoría de las tareas, el enfoque descrito funciona bastante bien, pero tarde o temprano tendrá que encontrarse con varios artefactos que genera el proceso de prefiltrado. Aquí están los más comunes y los métodos para tratarlos.La manifestación de las costuras del mapa cúbico.
La selección de valores del mapa en cubos procesado por el filtro preliminar para superficies con alta rugosidad conduce a la lectura de datos desde el nivel de mip en algún lugar más cercano al final de su cadena. Al muestrear desde un mapa cúbico, OpenGL por defecto no interpola linealmente entre las caras del mapa cúbico. Dado que los niveles altos de mip tienen una resolución más baja, y el mapa del entorno estaba enrevesado teniendo en cuenta un lóbulo espejo mucho más grande, la ausencia de filtrado de textura entre caras se hace evidente:Afortunadamente, OpenGL tiene la capacidad de activar este filtrado con una simple bandera: glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);
Es suficiente establecer el indicador en algún lugar del código de inicialización de la aplicación y este artefacto se elimina.Aparecen puntos brillantes
Dado que los reflejos de espejo en el caso general contienen detalles de alta frecuencia, así como regiones con brillos muy diferentes, su convolución requiere el uso de una gran cantidad de puntos de muestra para tener en cuenta correctamente la gran dispersión de valores dentro de los reflejos HDR del entorno. En el ejemplo, ya tomamos una cantidad suficientemente grande de muestras, pero para ciertas escenas y altos niveles de rugosidad del material, esto aún no será suficiente, y presenciará la aparición de muchos puntos alrededor de áreas brillantes:Puede continuar aumentando el número de muestras, pero esta no será una solución universal y, en algunas circunstancias, seguirá permitiendo un artefacto. Pero puede recurrir al método Chetan Jags , que le permite reducir la manifestación de un artefacto. Para hacer esto, en la etapa de convolución preliminar, la selección del mapa del entorno no se realiza directamente, sino a partir de uno de sus niveles de mip, en función del valor obtenido de la función de distribución de probabilidad del integrando y la rugosidad: float D = DistributionGGX(NdotH, roughness); float pdf = (D * NdotH / (4.0 * HdotV)) + 0.0001;
Solo recuerde habilitar el filtrado trilineal para el mapa del entorno para seleccionar con éxito de los niveles de mip: glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
Además, no olvide crear directamente niveles de mip para la textura usando OpenGL, pero solo después de que el nivel de mip principal esté completamente formado:
Este método funciona sorprendentemente bien, eliminando casi todos (y a menudo todos) los puntos del mapa filtrado, incluso a altos niveles de aspereza.Cálculo preliminar de BRDF
Por lo tanto, procesamos con éxito el mapa del entorno con el filtro y ahora podemos concentrarnos en la segunda parte de la aproximación en forma de una suma separada, que es BRDF. Para actualizar la memoria, revise nuevamente el registro completo de la solución aproximada:L o ( p , ω o ) = ∫ Ω L i ( p , ω i ) d ω i ∗ ∫ Ω f r ( p , ω i , ω o ) n ⋅ ω i d ω i
Calculamos preliminarmente la parte izquierda de la suma y registramos los resultados para varios niveles de rugosidad en un mapa cúbico separado. El lado derecho requerirá la convolución de la expresión BDRF junto con los siguientes parámetros: ángulon ⋅ ω i , rugosidad de la superficie y coeficiente de FresnelF 0 .
Un proceso similar a la integración de un BRDF reflejado para un entorno completamente blanco o con brillo de energía constante L i = 1.0 .
Convolver BRDF para tres variables no es una tarea trivial, pero en este caso F 0 puede derivarse de la expresión que describe el espejo BRDF:∫ Ω fr(p,ωi,ωo)n⋅ωidωi= ∫ Ω fr(p,ωi,ωo) F ( ω o , h )F ( ω o , h ) n⋅ωidωi
Aqui F es una función que describe el cálculo del conjunto de Fresnel. Moviendo el divisor a la expresión para BRDF, puede ir a la siguiente notación equivalente:∫ Ω f r ( p , ω i , ω o )F ( ω o , h ) F(ωo,h)n⋅ωidωi
Sustitución de la entrada correcta F en la aproximación de Fresnel-Schlick, obtenemos:∫ Ω f r ( p , ω i , ω o )F ( ω o , h ) (F0+(1-F0)(1-ωo⋅h)5)n⋅ωidωi
Denota la expresión ( 1 - ω o ⋅ h ) 5 como
/ a l p h a para simplificar la decisión sobreF 0 :
∫Ωfr(p,ωi,ωo)F(ωo,h)(F0+(1−F0)α)n⋅ωidωi
∫Ωfr(p,ωi,ωo)F(ωo,h)(F0+1∗α−F0∗α)n⋅ωidωi
∫Ωfr(p,ωi,ωo)F ( ω o , h ) (F0∗(1-α)+α)n⋅ωidωi
Siguiente función Dividimos F en dos integrales:∫ Ω f r ( p , ω i , ω o )F ( ω o , h ) (F0∗(1-α))n⋅ωidωi+∫Ωfr(p,ωi,ωo)F ( ω o , h ) (α)n⋅ωidωi
De esta manera F 0 será constante debajo de la integral, y podemos sacarlo del signo de la integral. A continuación, revelaremosα en la expresión original y obtenga la entrada final para BRDF como una suma separada:F0∫Ωfr(p,ωi,ωo)(1−(1−ωo⋅h)5)n⋅ωidωi+∫Ωfr(p,ωi,ωo)(1−ωo⋅h)5n⋅ωidωi
Las dos integrales resultantes representan la escala y el desplazamiento del valor. F 0, respectivamente. Tenga en cuenta quef ( p , ω i , ω o ) contiene una ocurrenciaF , porque estas ocurrencias se cancelan entre sí y desaparecen de la expresión. Utilizando el enfoque ya desarrollado, la convolución BRDF se puede llevar a cabo junto con los datos de entrada: rugosidad y ángulo entre vectoresn y
w o .
Escribir el resultado en la textura 2D - tarjeta de BRDF complejación ( BRDF mapa integración ), que servirá como un valores de la tabla auxiliares para su uso en el shader final, que formarán el resultado final de la iluminación especular indirecta.El sombreador de convolución BRDF funciona en el plano, utilizando directamente coordenadas de textura bidimensionales como parámetros de entrada del proceso de convolución ( NdotV y rugosidad ). El código es notablemente similar a una convolución de prefiltrado, pero aquí el vector de muestra se procesa teniendo en cuenta la función geométrica BRDF y la expresión de aproximación de Fresnel-Schlick: vec2 IntegrateBRDF(float NdotV, float roughness) { vec3 V; Vx = sqrt(1.0 - NdotV*NdotV); Vy = 0.0; Vz = NdotV; float A = 0.0; float B = 0.0; vec3 N = vec3(0.0, 0.0, 1.0); const uint SAMPLE_COUNT = 1024u; for(uint i = 0u; i < SAMPLE_COUNT; ++i) { vec2 Xi = Hammersley(i, SAMPLE_COUNT); vec3 H = ImportanceSampleGGX(Xi, N, roughness); vec3 L = normalize(2.0 * dot(V, H) * H - V); float NdotL = max(Lz, 0.0); float NdotH = max(Hz, 0.0); float VdotH = max(dot(V, H), 0.0); if(NdotL > 0.0) { float G = GeometrySmith(N, V, L, roughness); float G_Vis = (G * VdotH) / (NdotH * NdotV); float Fc = pow(1.0 - VdotH, 5.0); A += (1.0 - Fc) * G_Vis; B += Fc * G_Vis; } } A /= float(SAMPLE_COUNT); B /= float(SAMPLE_COUNT); return vec2(A, B); } // ---------------------------------------------------------------------------- void main() { vec2 integratedBRDF = IntegrateBRDF(TexCoords.x, TexCoords.y); FragColor = integratedBRDF; }
Como puede ver, la convolución de BRDF se implementa como una disposición casi literal de los cálculos matemáticos anteriores. Se toman los parámetros de entrada de rugosidad y ángulo.θ , se forma un vector de muestra basado en la muestra por significación, procesado usando la función de geometría y la expresión de Fresnel transformada para BRDF. Como resultado, para cada muestra, la magnitud de la escala y el desplazamiento del valorF 0 , que al final se promedian y devuelven en la formavec2. En unalecciónteórica, se mencionó que el componente geométrico de BRDF es ligeramente diferente en el caso de calcular IBL, ya que el coeficientek se especifica de manera diferente:k d i r e c t = ( α + 1 ) 28
k I B L = α 22
Dado que la convolución BRDF es parte de la solución de la integral en el caso del cálculo de IBL, utilizaremos el coeficiente k I B L para calcular la función de geometría en el modelo Schlick-GGX: float GeometrySchlickGGX(float NdotV, float roughness) { float a = roughness; float k = (a * a) / 2.0; float nom = NdotV; float denom = NdotV * (1.0 - k) + k; return nom / denom; } // ---------------------------------------------------------------------------- float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) { float NdotV = max(dot(N, V), 0.0); float NdotL = max(dot(N, L), 0.0); float ggx2 = GeometrySchlickGGX(NdotV, roughness); float ggx1 = GeometrySchlickGGX(NdotL, roughness); return ggx1 * ggx2; }
Tenga en cuenta que el coeficiente k se calcula en función del parámetroa. Además, en este caso, el parámetro derugosidadno se cuadra al describir el parámetroa, que se realizó en otros lugares donde se aplicó este parámetro. No está seguro de dónde está el problema aquí: El Epic Games o en el trabajo original de Disney, pero vale la pena decir que se trata de una asignación directa del valor derugosidadparámetro deunaconduce a la creación de mapas que integran BRDF idénticos, presentado en la publicación de Epic Games.Además, los resultados de convolución BRDF se guardarán en forma de una textura 2D de tamaño 512x512: unsigned int brdfLUTTexture; glGenTextures(1, &brdfLUTTexture);
Según lo recomendado por Epic Games, aquí se usa un formato de textura de punto flotante de 16 bits. Asegúrese de establecer el modo de repetición en GL_CLAMP_TO_EDGE para evitar muestrear artefactos desde el borde.Luego, usamos el mismo objeto de frame buffer y ejecutamos un sombreador en la superficie de un quad de pantalla completa: glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); glBindRenderbuffer(GL_RENDERBUFFER, captureRBO); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 512, 512); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, brdfLUTTexture, 0); glViewport(0, 0, 512, 512); brdfShader.use(); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); RenderQuad(); glBindFramebuffer(GL_FRAMEBUFFER, 0);
Como resultado, obtenemos un mapa de textura que almacena el resultado de la convolución de la parte de la expresión de la cantidad dividida responsable de BRDF:Teniendo a mano los resultados del filtrado preliminar del mapa del entorno y la textura con los resultados de la convolución BRDF, podemos restaurar el resultado del cálculo de la integral para la iluminación especular indirecta basada en la aproximación por una suma separada. El valor restaurado se utilizará posteriormente como radiación especular indirecta o de fondo.Cálculo de reflectancia final en modelo IBL
Entonces, para obtener un valor que describa el componente espejo indirecto en la expresión general de reflectividad, es necesario "pegar" los componentes de aproximación calculados en un todo único como una suma separada. Primero, agregue los muestreadores apropiados al sombreador final para los datos precalculados: uniform samplerCube prefilterMap; uniform sampler2D brdfLUT;
Primero, obtenemos el valor de la reflexión especular indirecta en la superficie mediante el muestreo de un mapa de entorno preprocesado basado en el vector de reflexión. Tenga en cuenta que aquí la selección del nivel de mip para el muestreo se basa en la rugosidad de la superficie. Para superficies más rugosas, el reflejo será más borroso : void main() { [...] vec3 R = reflect(-V, N); const float MAX_REFLECTION_LOD = 4.0; vec3 prefilteredColor = textureLod(prefilterMap, R, roughness * MAX_REFLECTION_LOD).rgb; [...] }
En la etapa de convolución preliminar, preparamos solo 5 niveles de mip (de cero a cuarto), la constante MAX_REFLECTION_LOD sirve para limitar la selección de los niveles de mip generados.A continuación, hacemos una selección del mapa de integración BRDF en función de la rugosidad y el ángulo entre la normal y la dirección de visión: vec3 F = FresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness); vec2 envBRDF = texture(brdfLUT, vec2(max(dot(N, V), 0.0), roughness)).rg; vec3 specular = prefilteredColor * (F * envBRDF.x + envBRDF.y);
El valor obtenido del mapa contiene los factores de escala y desplazamiento para el valor. F 0 (aquí tomamos el valorF- Coeficiente de Fresnel). El valor convertidoFluego se combina con el valor obtenido del mapa de prefiltrado para obtener una solución aproximada de la expresión integral original -especular.Por lo tanto, obtenemos una solución para la parte de la expresión de reflectividad, que es responsable de la reflexión especular. Para obtener una solución completa del modelo PBR IBL, debe combinar este valor con la solución para la parte difusa de la expresión de reflectancia que recibimos en laúltimalección: vec3 F = FresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness); vec3 kS = F; vec3 kD = 1.0 - kS; kD *= 1.0 - metallic; vec3 irradiance = texture(irradianceMap, N).rgb; vec3 diffuse = irradiance * albedo; const float MAX_REFLECTION_LOD = 4.0; vec3 prefilteredColor = textureLod(prefilterMap, R, roughness * MAX_REFLECTION_LOD).rgb; vec2 envBRDF = texture(brdfLUT, vec2(max(dot(N, V), 0.0), roughness)).rg; vec3 specular = prefilteredColor * (F * envBRDF.x + envBRDF.y); vec3 ambient = (kD * diffuse + specular) * ao;
Observo que el valor especular no se multiplica por kS , ya que ya contiene un coeficiente de Fresnel.Ejecutemos nuestra aplicación de prueba con un conjunto familiar de esferas con características cambiantes de metalicidad y aspereza y echemos un vistazo a su apariencia en todo el esplendor de PBR:Puede ir aún más lejos y descargar un conjunto de texturas correspondientes al modelo PBR y obtener esferas de materiales reales :O incluso descargue un modelo magnífico junto con texturas PBR preparadas de Andrew Maximov :Creo que no tendrá que convencer a nadie de que el modelo de iluminación actual parece mucho más convincente. Y lo que es más, la iluminación se ve físicamente correcta independientemente del mapa del entorno. A continuación, se utilizan varios mapas de entorno HDR completamente diferentes que cambian por completo la naturaleza de la iluminación, pero todas las imágenes se ven físicamente confiables, ¡a pesar de que no tuvo que ajustar ningún parámetro en el modelo! (En principio, esta simplificación del trabajo con materiales es la principal ventaja de la tubería PBR, y se puede decir que una mejor imagen es una consecuencia agradable. Nota ) .Fuh, nuestro viaje a la esencia del renderizador PBR es bastante voluminoso. Llegamos al resultado a través de una serie de pasos y, por supuesto, muchas cosas pueden salir mal con los primeros enfoques. Por lo tanto, para cualquier problema, le aconsejo que comprenda cuidadosamente el código de muestra para esferas monocromáticas y texturizadas (¡y el código del sombreador, por supuesto!). O pedir consejo en los comentarios.Que sigue
Espero que al leer estas líneas ya haya desarrollado una comprensión del trabajo del modelo de renderizado PBR, así como haya descubierto y lanzado con éxito una aplicación de prueba. En estas lecciones, calculamos todos los mapas de textura auxiliares necesarios para el modelo PBR en nuestra aplicación antes del ciclo de renderizado principal. Para las tareas de capacitación, este enfoque es adecuado, pero no para la aplicación práctica. En primer lugar, dicha preparación preliminar debe ocurrir una vez, y no cada lanzamiento de la aplicación. En segundo lugar, si decide agregar más mapas de entorno, también tendrá que procesarlos al inicio. ¿Qué pasa si se agregan algunas cartas más? Bola de nieve real.Es por eso que, en el caso general, un mapa de irradiación y un mapa del entorno preprocesado se preparan una vez y luego se guardan en el disco (el mapa de agregación BRDF no depende del mapa del entorno, por lo que generalmente se puede calcular o descargar una vez). Se deduce que necesitará un formato para almacenar tarjetas cúbicas HDR, incluidos sus niveles de mip. Bueno, o puede almacenarlos y cargarlos utilizando uno de los formatos más utilizados (por lo que .dds admite guardar niveles de mip ).Otro punto importante: con el fin de dar una comprensión profunda de la tubería PBR en estas lecciones, describí el proceso completo de preparación para el render PBR, incluidos los cálculos preliminares de tarjetas auxiliares para IBL. Sin embargo, en su práctica, también puede utilizar una de las excelentes utilidades que preparan estas tarjetas para usted: por ejemplo cmftStudio o IBLBaker .Tampoco se consideran los procesos de elaboración de los mapas de cubo muestras reflexiones ( sondas de reflexión) y los procesos relacionados de interpolación de mapas cúbicos y corrección de paralaje. Brevemente, esta técnica se puede describir de la siguiente manera: colocamos en nuestra escena muchos objetos de muestras de reflexión, que forman una imagen ambiental local en forma de un mapa cúbico, y luego todos los mapas auxiliares necesarios para el modelo IBL se forman sobre esta base. Al interpolar datos de varias muestras en función de la distancia desde la cámara, puede obtener una iluminación muy detallada basada en la imagen, cuya calidad está esencialmente limitada solo por la cantidad de muestras que estamos listos para colocar en la escena. Este enfoque le permite cambiar correctamente la iluminación, por ejemplo, al pasar de una calle bien iluminada al atardecer de una habitación determinada. Probablemente voy a escribir una lección sobre pruebas de reflexión en el futuro,Sin embargo, por el momento, solo puedo recomendar el artículo de Chetan Jags a continuación para su revisión.(La implementación de muestras, y mucho más, se puede encontrar en el motor sin procesar del autor de los tutoriales aquí , aprox. Por. )Materiales adicionales
- Sombreado real en Unreal Engine 4 : una explicación del enfoque de Epic Games para aproximar la expresión del componente espejo por una suma dividida. Basado en este artículo, se escribió el código para la lección IBL PBR.
- Sombreado físico e iluminación basada en imágenes : un excelente artículo que describe el proceso de incorporar el cálculo del componente espejo de IBL en una aplicación interactiva de tuberías PBR.
- Iluminación basada en imágenes : una publicación muy larga y detallada sobre IBL especular y cuestiones relacionadas, incluido el problema de interpolación de la sonda de luz.
- Moving Frostbite to PBR : , PBR «AAA».
- Physically Based Rendering – Part Three : , IBL PBR JMonkeyEngine.
- Implementation Notes: Runtime Environment Map Filtering for Image Based Lighting : HDR , .