wandres.dev
PBR I · La teoría del renderizado físico

Qué es una BRDF y qué tiene que cumplir

La función que define un material, las tres condiciones que la hacen físicamente válida, y el modelo de microfacetas D-F-G tal y como está escrito en el shader de Three.js r184.

⏱ 20 min

Un material, en un motor moderno, no es una lista de propiedades: es una función de cuatro variables angulares que responde a una sola pregunta, qué fracción de la luz que llega desde aquí se va hacia allá. Esa función es la BRDF, y las tres condiciones que debe cumplir para ser físicamente válida son precisamente las que los modelos anteriores al PBR incumplían.

🎯 Al terminar esta lección sabrás
  • Definir la BRDF con su significado dimensional y sus tres condiciones.
  • Deducir de dónde sale el factor 1 / PI de la BRDF lambertiana.
  • Describir los tres términos del modelo de microfacetas y qué controla cada uno.
  • Localizar D_GGX, F_Schlick y V_GGX_SmithCorrelated en el shader real.

La definición y las tres condiciones

La BRDF, fr(wi, wo), es el cociente entre la radiancia diferencial que sale hacia wo y la irradiancia diferencial que llega desde wi. Sus unidades son estereorradián a la menos uno, lo que ya avisa de algo importante: no es una probabilidad y puede valer más que uno. Un espejo tiene una BRDF que tiende a infinito en la dirección especular y vale cero en el resto.

Para que una BRDF describa un material que podría existir tiene que cumplir tres cosas.

No negatividad. No puede devolver valores negativos. Obvio, pero varios modelos empíricos antiguos lo incumplen en ángulos rasantes y producen píxeles negros.

Reciprocidad de Helmholtz. Intercambiar la dirección de entrada y la de salida no cambia el resultado. Si inviertes el camino de la luz, la fracción reflejada es la misma. Es la razón por la que un trazador de caminos puede lanzar rayos desde la cámara en lugar de desde la luz, y por eso un modelo que no la cumple no se puede usar en un trazador bidireccional.

Conservación de energía. La integral de la BRDF por el coseno sobre todo el hemisferio de salida no puede superar uno para ninguna dirección de entrada. Una superficie no puede devolver más luz de la que recibe. Esta es la que rompía Blinn-Phong, y tiene lección propia.

De dónde sale el uno partido por pi

La BRDF lambertiana es constante: refleja igual en todas las direcciones. Si la superficie tiene albedo a, es decir, refleja una fracción a de la energía que recibe, ¿cuánto vale esa constante?

Imponemos la conservación: la integral de fr * cos(theta) sobre el hemisferio tiene que valer a. Con fr constante, sale fuera de la integral, y la integral del coseno sobre el hemisferio vale exactamente pi. Por tanto fr * pi = a, y de ahí fr = a / pi.

Ese factor no es un ajuste de intensidad ni una convención de Three.js: es la única constante que hace que una superficie mate devuelva exactamente lo que promete su albedo. En el shader está tal cual:

vec3 BRDF_Lambert( const in vec3 diffuseColor ) {
  return RECIPROCAL_PI * diffuseColor;
}

Si alguna vez ves un shader propio en el que el difuso se ve el triple de brillante que el de Three.js, casi siempre es que falta ese 1 / PI.

El modelo de microfacetas

Para el especular no vale una constante. El modelo que usa todo el mundo desde Cook y Torrance parte de una idea física: una superficie rugosa es, a escala microscópica, un conjunto enorme de espejos diminutos orientados en distintas direcciones. Un fragmento no es una superficie lisa, es una estadística de orientaciones.

De ahí salen tres factores, y cada uno responde a una pregunta distinta.

D, la distribución de normales. ¿Qué fracción de esos microespejos está orientada exactamente en la dirección que haría rebotar la luz de wi a wo? Esa dirección es el vector medio entre las dos, el half vector. D es una función de densidad sobre orientaciones, y es lo que produce la forma del brillo: estrecha si la superficie es lisa, ancha si es rugosa. Three.js usa GGX, también llamada Trowbridge-Reitz, que tiene una cola larga característica y por eso los brillos GGX tienen un halo suave alrededor del núcleo que los de Blinn-Phong no tienen.

F, el Fresnel. ¿Qué fracción de la luz refleja realmente ese microespejo, según el ángulo? Todas las superficies reflejan más en incidencia rasante, hasta llegar al cien por cien cuando miras casi paralelo a la superficie. Es el efecto que ves en el agua de un lago: transparente a tus pies, espejo en el horizonte. Three.js usa la aproximación de Schlick, que interpola entre la reflectancia en incidencia normal, F0, y uno, con una quinta potencia.

G, la geometría, o su versión normalizada V. Los microespejos se tapan y se sombrean entre sí. Con la superficie muy rugosa y la luz muy rasante, buena parte de las microfacetas orientadas correctamente están escondidas detrás de otras. G estima esa fracción visible. Three.js usa la variante de Smith con correlación de altura, que modela conjuntamente el sombreado y el enmascaramiento en vez de asumirlos independientes.

La BRDF especular completa es D * F * G dividido por 4 * cos(theta_i) * cos(theta_o). En el shader de r184 aparece más compacta porque el denominador está absorbido en V:

vec3 BRDF_GGX( ... ) {
  float alpha = pow2( roughness );          // reparametrizacion de Disney

  vec3 halfDir = normalize( lightDir + viewDir );
  float dotNL = saturate( dot( normal, lightDir ) );
  float dotNV = saturate( dot( normal, viewDir ) );
  float dotNH = saturate( dot( normal, halfDir ) );
  float dotVH = saturate( dot( viewDir, halfDir ) );

  vec3  F = F_Schlick( f0, f90, dotVH );
  float V = V_GGX_SmithCorrelated( alpha, dotNL, dotNV );
  float D = D_GGX( alpha, dotNH );

  return F * ( V * D );
}

Los cuatro productos escalares de arriba son toda la información geométrica que necesita el modelo. Cambiar de material es cambiar f0, f90 y alpha; la estructura no se mueve.

alpha es roughness al cuadrado, y eso es una decisión de interfaz, no de física

Mira la primera línea: float alpha = pow2( roughness ). El parámetro que entra de verdad en D_GGX y en V_GGX_SmithCorrelated no es tu roughness, es su cuadrado. Esa reparametrización viene del artículo de Disney de 2012 y su motivo es puramente perceptual: con alpha lineal, casi toda la variación visible del brillo ocurre entre 0 y 0.15, y el resto del deslizador no hace nada apreciable. Elevando al cuadrado, el cambio percibido se reparte de forma aproximadamente uniforme a lo largo del rango. Consecuencias prácticas: roughness = 0.5 no es “medio rugoso” en términos físicos, es alpha = 0.25, bastante más pulido de lo que el número sugiere; y si conviertes materiales desde un motor que expone alpha directamente, tienes que aplicar la raíz cuadrada o todo saldrá demasiado brillante. Es también la razón por la que interpolar roughness linealmente en una textura no interpola linealmente el aspecto, y por la que un roughnessMap con degradado suave puede producir una transición visualmente brusca.

Lo que la BRDF no puede describir

Conviene saber los límites del modelo, porque explican por qué existen las capas de MeshPhysicalMaterial.

Una BRDF describe reflexión en el mismo punto. No describe luz que entra por un sitio, viaja bajo la superficie y sale por otro: eso es dispersión subsuperficial y hace falta una BSSRDF, con dos posiciones además de las dos direcciones. La piel, el mármol, la leche y la cera necesitan eso, y ningún material estándar de Three.js lo hace de verdad.

Tampoco describe transmisión: la luz que atraviesa. Para eso hace falta el hemisferio de abajo, y la función pasa a llamarse BSDF. Es lo que activa transmission en MeshPhysicalMaterial.

Y no describe interferencia ni difracción, que son efectos ondulatorios. El arcoíris de una pompa de jabón o de un CD no sale de ninguna BRDF de microfacetas; hay que añadir un modelo de película fina, que es exactamente lo que hace iridescence.

Cada una de esas limitaciones es una capa del material físico, y por eso las capas no son “efectos”: son términos que el modelo base no puede producir por mucho que muevas los deslizadores.