
La lección
anterior dio una visión general de los conceptos básicos de la implementación de un modelo de representación físicamente plausible. Esta vez pasaremos de los cálculos teóricos a una implementación de renderización específica con la participación de fuentes de luz directa (analítica): punto, direccional o reflector.
Primero, vamos a actualizar la expresión para calcular la reflectividad de la lección anterior:
En su mayor parte, ya hemos tratado los componentes de esta fórmula, pero la pregunta sigue siendo cómo representar específicamente la
irradiancia , que es el brillo total de la energía (
resplandor )
toda la escena Acordamos que el brillo de la energía
(en términos de terminología de gráficos por computadora) se considera como la relación del flujo de radiación (
flujo radiante )
(energía de radiación de la fuente de luz) al valor del ángulo sólido
. En nuestro caso, el ángulo sólido
lo consideramos infinitesimal y, por lo tanto, el brillo de la energía da una idea del flujo de radiación para cada rayo de luz individual (su dirección).
¿Cómo vincular estos cálculos con el modelo de iluminación que conocemos de lecciones anteriores? Primero, imagine que recibe una fuente de luz de un solo punto (que emite de manera uniforme en todas las direcciones) con un flujo de radiación definido como una tríada RGB (23.47, 21.31, 20.79). La
intensidad radiante
de dicha fuente es igual a su flujo de radiación en todas las direcciones. Sin embargo, habiendo considerado el problema de determinar el color de un punto específico
en la superficie, se puede ver que de todas las posibles direcciones de incidencia de luz en el hemisferio
solo vector
obviamente vendrá de una fuente de luz. Como solo se representa una fuente de luz, representada por un punto en el espacio, para todas las demás direcciones posibles de incidencia de luz en un punto
El brillo de la energía será igual a cero:
Ahora, si no tenemos en cuenta temporalmente la ley de atenuación de la luz para una fuente determinada, resulta que el brillo de la energía para el haz de luz incidente de esta fuente permanece sin cambios donde sea que coloquemos la fuente (escala de luminosidad basada en el coseno del ángulo de incidencia
tampoco cuenta). En total, una fuente puntual mantiene constante la fuerza de radiación independientemente del ángulo de visión, lo que equivale a tomar la fuerza de radiación igual al flujo de radiación inicial en forma de una tríada constante (23.47, 21.31, 20.79).
Sin embargo, el cálculo del brillo de la energía también se basa en la coordenada del punto.
, al menos cualquier fuente de luz físicamente confiable exhibe atenuación de la fuerza de radiación al aumentar la distancia desde un punto a una fuente. También debe tener en cuenta la orientación de la superficie, como se puede ver en la expresión original de luminosidad: el resultado del cálculo de la fuerza de radiación se debe multiplicar por el valor escalar del vector normal a la superficie
y vector de incidencia de radiación
.
Para reescribir lo anterior: para una fuente de luz directa, la función de radiación
determina el color de la luz incidente, teniendo en cuenta la atenuación a una distancia dada del punto
y teniendo en cuenta la escala por un factor
pero solo para un solo rayo de luz
llegando al punto
- esencialmente el único vector que conecta la fuente y el punto. En forma de código fuente, esto se interpreta de la siguiente manera:
vec3 lightColor = vec3(23.47, 21.31, 20.79); vec3 wi = normalize(lightPos - fragPos); float cosTheta = max(dot(N, Wi), 0.0); float attenuation = calculateAttenuation(fragPos, lightPos); vec3 radiance = lightColor * attenuation * cosTheta;
Si cierra los ojos a una terminología ligeramente modificada, este código debería recordarle algo. Sí, sí, este es el mismo código para calcular el componente difuso en el modelo de iluminación que conocemos. Para la iluminación directa, el brillo de la energía está determinado por un solo vector para la fuente de luz, porque el cálculo se lleva a cabo de una manera tan similar a la que todavía conocemos.
Observo que esta afirmación es verdadera solo bajo el supuesto de que una fuente puntual de luz es infinitesimal y está representada por un punto en el espacio. Al modelar una fuente de volumen, su luminosidad diferirá de cero en muchas direcciones, y no solo en un haz.
Para otras fuentes de luz que emiten radiación desde un solo punto, el brillo de la energía se calcula de la misma manera. Por ejemplo, una fuente de luz direccional tiene una dirección constante
y no usa atenuación, y la fuente de proyección muestra una potencia de radiación variable, dependiendo de la dirección de la fuente.
Aquí volvemos al valor de la integral.
en la superficie del hemisferio
. Como sabemos de antemano las posiciones de todas las fuentes de luz que participan en el sombreado de un punto en particular, no necesitamos tratar de resolver la integral. Podemos calcular directamente la irradiación total proporcionada por este número de fuentes de luz, ya que el brillo de la energía de la superficie se ve afectado por una sola dirección para cada fuente.
Como resultado, el cálculo de PBR para fuentes de luz directa es una cuestión bastante simple, ya que todo se reduce a una búsqueda secuencial de las fuentes involucradas en la iluminación. Más tarde, aparecerá un componente del entorno en el modelo de iluminación, en el que trabajaremos en el tutorial sobre iluminación basada en imágenes (Iluminación basada en imágenes,
IBL ). No se puede escapar de la estimación de la integral, ya que la luz en dicho modelo cae desde muchas direcciones.
Modelo de superficie PBR
Comencemos con el sombreador de fragmentos que implementa el modelo PBR descrito anteriormente. Primero, establecemos los datos de entrada necesarios para el sombreado de la superficie:
#version 330 core out vec4 FragColor; in vec2 TexCoords; in vec3 WorldPos; in vec3 Normal; uniform vec3 camPos; uniform vec3 albedo; uniform float metallic; uniform float roughness; uniform float ao;
Aquí puede ver la entrada habitual calculada utilizando el sombreador de vértices más simple, así como un conjunto de uniformes que describen las características de la superficie del objeto.
Además, al comienzo del código de sombreador, realizamos cálculos que son tan familiares por la implementación del modelo de iluminación Blinn-Fong:
void main() { vec3 N = normalize(Normal); vec3 V = normalize(camPos - WorldPos); [...] }
Iluminación directa
El ejemplo de esta lección contiene solo cuatro fuentes de luz puntuales que especifican claramente la irradiación de la escena. Para satisfacer la expresión de reflectividad, revisamos iterativamente cada fuente de luz, calculamos el brillo de energía individual y resumimos esta contribución, modulando simultáneamente el valor BRDF y el ángulo de incidencia del haz de luz. Puedes imaginar esta iteración como una solución de la integral sobre la superficie
Solo para fuentes de luz analíticas.
Entonces, primero calculamos los valores calculados para cada fuente:
vec3 Lo = vec3(0.0); for(int i = 0; i < 4; ++i) { vec3 L = normalize(lightPositions[i] - WorldPos); vec3 H = normalize(V + L); float distance = length(lightPositions[i] - WorldPos); float attenuation = 1.0 / (distance * distance); vec3 radiance = lightColors[i] * attenuation; [...]
Dado que los cálculos se llevan a cabo en un espacio lineal (
la corrección gamma se realiza al final del sombreador), se usa una ley de atenuación más físicamente correcta de acuerdo con el cuadrado inverso de la distancia:
Suponga que la ley del cuadrado inverso es más físicamente correcta, para controlar mejor la naturaleza del amortiguamiento, es bastante posible usar la fórmula ya familiar que contiene términos constantes, lineales y cuadráticos.
Además, para cada fuente, también calculamos el valor del espejo Cook-Torrance BRDF:
El primer paso es calcular la relación entre la reflexión especular y difusa, o, en otras palabras, la relación entre la cantidad de luz reflejada y la cantidad de luz refractada por la superficie. De la
lección anterior, sabemos cómo se ve el cálculo del coeficiente de Fresnel:
vec3 fresnelSchlick(float cosTheta, vec3 F0) { return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); }
La aproximación de Fresnel-Schlick espera el parámetro
F0 en la entrada, que muestra el
grado de reflexión de la superficie con un ángulo cero de incidencia de luz , es decir. grado de reflexión, si observa la superficie a lo largo de la normal de arriba a abajo. El valor de
F0 varía según el material y adquiere una tonalidad de color para metales, como se puede ver mirando los catálogos de materiales PBR. Para el proceso de
flujo de trabajo metálico (proceso de autoría de materiales PBR, dividiendo todos los materiales en clases de dieléctricos y conductores), se supone que todos los dieléctricos se ven bastante confiables a un valor constante de
F0 = 0.04 , mientras que para las superficies metálicas
F0 se establece en función del albedo de la superficie. En forma de código:
vec3 F0 = vec3(0.04); F0 = mix(F0, albedo, metallic); vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0);
Como puede ver, para superficies estrictamente no metálicas,
F0 se establece igual a 0.04. Pero al mismo tiempo, puede cambiar suavemente de este valor al valor de albedo en función de la "metalicidad" de la superficie. Este indicador generalmente se presenta como una textura separada (de aquí, de hecho, se toma el flujo de trabajo
metálico ,
aprox. Trans. ).
Habiendo recibido
necesitamos calcular el valor de la función de distribución normal
y funciones de geometría
:
Código de función para el caso con iluminación analítica:
float DistributionGGX(vec3 N, vec3 H, float roughness) { float a = roughness*roughness; float a2 = a*a; float NdotH = max(dot(N, H), 0.0); float NdotH2 = NdotH*NdotH; float num = a2; float denom = (NdotH2 * (a2 - 1.0) + 1.0); denom = PI * denom * denom; return num / denom; } float GeometrySchlickGGX(float NdotV, float roughness) { float r = (roughness + 1.0); float k = (r*r) / 8.0; float num = NdotV; float denom = NdotV * (1.0 - k) + k; return num / 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; }
Una diferencia importante con respecto a la descrita en la
parte teórica : aquí pasamos directamente el parámetro de rugosidad a todas las funciones mencionadas. Esto se hace para permitir que cada función modifique el valor de rugosidad original a su manera. Por ejemplo, los estudios de Disney, reflejados en el motor de Epic Games, mostraron que el modelo de iluminación da resultados visualmente más correctos si utilizamos el cuadrado de la rugosidad en la función de geometría y la función de distribución normal.
Una vez configuradas todas las funciones, se pueden obtener directamente los valores de NDF y G:
float NDF = DistributionGGX(N, H, roughness); float G = GeometrySmith(N, V, L, roughness);
Total tenemos a mano todos los valores para calcular todo el BRDF Cook-Torrance:
vec3 numerator = NDF * G * F; float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.001; vec3 specular = numerator / denominator;
Tenga en cuenta que limitamos el denominador a un valor mínimo de 0.001 para evitar la división por cero en casos de poner a cero el producto escalar.
Ahora procedemos a calcular la contribución de cada fuente a la ecuación de reflectividad. Dado que el coeficiente de Fresnel es directamente una variable
, entonces podemos usar el valor de F para indicar la contribución de la fuente a la reflexión especular de la superficie. De la cantidad
se puede obtener y el índice de refracción
:
vec3 kS = F; vec3 kD = vec3(1.0) - kS; kD *= 1.0 - metallic;
Como consideramos la cantidad
kS que representa la cantidad de energía luminosa como una superficie reflejada, restándola de la unidad, obtenemos la energía residual de la luz
kD refractada por la superficie. Además, dado que los metales no refractan la luz y no tienen un componente difuso de luz reemitida, el componente
kD se modulará para que sea cero para un material totalmente metálico. Después de estos cálculos, tendremos todos los datos a mano para calcular la reflectancia proporcionada por cada una de las fuentes de luz:
const float PI = 3.14159265359; float NdotL = max(dot(N, L), 0.0); Lo += (kD * albedo / PI + specular) * radiance * NdotL; }
El valor final
Lo , o brillo de energía saliente, es esencialmente una solución para la expresión de la reflectividad, es decir. resultado de integración de superficie
. En este caso, no necesitamos tratar de resolver la integral de forma general para todas las direcciones posibles, ya que en este ejemplo solo hay cuatro fuentes de luz que afectan el fragmento que se procesa. Es por eso que toda "integración" se limita a un ciclo simple de fuentes de luz existentes.
Solo queda agregar la similitud del componente de iluminación de fondo a los resultados del cálculo de la fuente de luz directa y el color final del fragmento está listo:
vec3 ambient = vec3(0.03) * albedo * ao; vec3 color = ambient + Lo;
Renderizado lineal y HDR
Hasta ahora, asumimos que todos los cálculos se llevan a cabo en un espacio de color lineal y, por lo tanto, utilizamos
la corrección gamma como el acorde final en nuestro sombreador. La realización de cálculos en el espacio lineal es extremadamente importante para la simulación correcta de PBR, ya que el modelo requiere la linealidad de todos los datos de entrada. Intente no garantizar la linealidad de ninguno de los parámetros y el resultado del sombreado será incorrecto. Además, sería bueno establecer las fuentes de luz con características cercanas a las fuentes reales: por ejemplo, el color de su radiación y el brillo de la energía pueden variar libremente en un amplio rango. Como resultado,
Lo puede aceptar fácilmente valores grandes, pero inevitablemente cae dentro del límite en el intervalo [0., 1.] debido al bajo rango dinámico (
LDR ) del búfer de trama predeterminado.
Para evitar la pérdida de los valores de HDR, antes de la corrección gamma, es necesario realizar una compresión de tono:
color = color / (color + vec3(1.0)); color = pow(color, vec3(1.0/2.2));
Aquí se utiliza el conocido operador Reinhardt, lo que nos permite mantener un amplio rango dinámico en condiciones de irradiación muy cambiante de diferentes partes de la imagen. Como aquí no utilizamos un sombreador separado para el procesamiento posterior, las operaciones descritas se pueden agregar simplemente al final del código del sombreador.
Repito que para el modelado correcto de PBR es extremadamente importante recordar y considerar las características de trabajar con espacio de color lineal y renderizado HDR. El descuido de estos aspectos conducirá a cálculos incorrectos y resultados visualmente estéticos.
Sombreador PBR para iluminación analítica
Entonces, junto con los toques finales en forma de compresión tonal y corrección gamma, solo queda transferir el color final del fragmento a la salida del sombreador de fragmentos y el código del sombreador PBR para iluminación directa puede considerarse completado. Finalmente, echemos un vistazo al código completo de la función
main () de este sombreador:
#version 330 core out vec4 FragColor; in vec2 TexCoords; in vec3 WorldPos; in vec3 Normal;
Espero que después de leer la
parte teórica y con el análisis actual de la expresión de la capacidad reflexiva, esa lista deje de parecer intimidante.
Utilizamos este sombreador en una escena que contiene cuatro fuentes de luz puntuales, un cierto número de esferas cuyas características de superficie cambiarán el grado de rugosidad y metalicidad a lo largo de los ejes horizontal y vertical, respectivamente. En la salida obtenemos la siguiente imagen:
La metalicidad cambia de cero a uno de abajo hacia arriba, y la rugosidad es similar, pero de izquierda a derecha. Se hace evidente que cambiando solo estas dos características de la superficie, ya es posible establecer una amplia gama de materiales.
El código fuente completo está
aquí .
PBR y texturizado
Ampliaremos nuestro modelo de superficie transmitiendo características en forma de texturas. De esta manera, podemos proporcionar control por fragmento de los parámetros del material de superficie:
[...] uniform sampler2D albedoMap; uniform sampler2D normalMap; uniform sampler2D metallicMap; uniform sampler2D roughnessMap; uniform sampler2D aoMap; void main() { vec3 albedo = pow(texture(albedoMap, TexCoords).rgb, 2.2); vec3 normal = getNormalFromNormalMap(); float metallic = texture(metallicMap, TexCoords).r; float roughness = texture(roughnessMap, TexCoords).r; float ao = texture(aoMap, TexCoords).r; [...] }
Tenga en cuenta que la textura del albedo de la superficie generalmente es creada por artistas en el espacio de color sRGB, por lo que en el código anterior devolvemos el color del texel al espacio lineal para que pueda usarse en cálculos posteriores. Dependiendo de cómo los artistas crean la textura que contiene los datos del
mapa de oclusión ambiental , también puede tener que llevarse al espacio lineal. Los mapas de metalicidad y rugosidad casi siempre se crean en un espacio lineal.
El uso de texturas en lugar de parámetros de superficie fijos en combinación con el algoritmo PBR proporciona un aumento significativo en la fiabilidad visual en comparación con los algoritmos de iluminación utilizados anteriormente:
El código de ejemplo de texturas completo está
aquí , y las texturas utilizadas están
aquí (junto con la textura de sombreado de fondo). Le llamo la atención sobre el hecho de que las superficies fuertemente metálicas aparecen oscurecidas bajo condiciones de iluminación directa, ya que la contribución de la reflexión difusa es pequeña (en el límite no hay ninguna). Su sombreado se vuelve más correcto solo cuando se tiene en cuenta el reflejo del espejo de la iluminación del entorno, lo que haremos en las próximas lecciones.
Por el momento, el resultado puede no ser tan impresionante como algunas demostraciones de PBR; sin embargo, todavía no hemos implementado un sistema de iluminación basado en imágenes (
IBL ). Sin embargo, nuestro render ahora se considera basado en principios físicos e, incluso sin IBL, muestra una imagen más confiable que antes.