Conservación de energía y por qué importa tanto
Qué energía se pierde en un modelo de microfacetas de un solo rebote, cómo la recupera Three.js r184, y qué rango de valores de albedo son posibles en la realidad.
La conservación de energía es la condición que separa un modelo físico de uno empírico, y su efecto no es sutil: es la diferencia entre un material que se comporta bien bajo cualquier luz y uno que hay que reajustar en cada escena. En un motor moderno no la mantienes tú a base de bajar deslizadores; la impone la matemática del modelo, y donde el modelo la pierde, el motor la compensa explícitamente.
- Explicar cómo se reparte la energía entre el término especular y el difuso.
- Justificar por qué un metal no tiene componente difusa en absoluto.
- Describir la energía que pierde una BRDF de un solo rebote y cómo la recupera r184.
- Acotar el rango de valores de albedo que corresponden a materiales reales.
El reparto entre especular y difuso
Cuando la luz llega a un dieléctrico, una parte se refleja en la interfaz sin entrar, y esa parte es el especular. La otra entra en el material, rebota entre sus partículas, pierde las longitudes de onda que el pigmento absorbe, y sale de nuevo en una dirección más o menos aleatoria. Eso es el difuso.
Las dos partes son la misma energía repartida. Cuánta va a cada lado lo decide el Fresnel: en incidencia normal, un dieléctrico típico refleja alrededor del cuatro por ciento y deja entrar el noventa y seis; en incidencia rasante refleja casi todo. Por tanto el difuso no puede ser independiente del especular, tiene que ser lo que el especular no se llevó.
Three.js lo hace explícitamente en el término indirecto:
vec3 totalScatteringDielectric = singleScatteringDielectric + multiScatteringDielectric;
vec3 diffuse = material.diffuseContribution * ( 1.0 - totalScatteringDielectric );
Ese 1.0 - es la conservación escrita en una línea. La energía que se ha reflejado especularmente se resta de la disponible para el difuso. En Blinn-Phong no existe nada equivalente: el especular se suma encima del difuso, y por eso un material Phong con difuso blanco y especular blanco devuelve casi el doble de la luz que recibe.
Por qué un metal no tiene difuso
En un metal, los electrones libres reaccionan al campo electromagnético de forma que la luz que penetra se absorbe en una distancia de unos pocos nanómetros. Nada vuelve a salir. No hay rebote subsuperficial, no hay pigmento, no hay difuso. Todo lo que ves de un metal es su reflexión especular, y el color del oro o del cobre está en esa reflexión, no en un albedo.
Three.js lo implementa sin medias tintas:
material.diffuseContribution = diffuseColor.rgb * ( 1.0 - metalnessFactor );
material.specularColorBlended = mix( material.specularColor, diffuseColor.rgb, metalnessFactor );
Con metalness a uno, la contribución difusa es exactamente cero y el F0 especular pasa a ser el color base. Ese doble uso del mismo canal es lo que hace que el flujo metallic-roughness funcione con solo tres canales de color: baseColor significa albedo en un dieléctrico y F0 en un metal, y metalness decide cuál de las dos lecturas se aplica.
De ahí sale una consecuencia práctica que sorprende: subir metalness en un material de color oscuro lo hace más oscuro, no más brillante, porque le quitas el difuso y le das un F0 oscuro que refleja poco. Un metal negro es casi invisible sin un entorno que reflejar. Si quieres oro, el color base tiene que ser el color del oro reflejado, que es un amarillo bastante claro y saturado, no un marrón.
La energía que se pierde y cómo se recupera
El modelo de microfacetas clásico calcula un solo rebote: la luz llega a un microespejo, sale, se acabó. Pero en una superficie rugosa un rayo rebota varias veces entre microfacetas antes de escapar. Toda esa energía de rebotes múltiples simplemente no se contabiliza, y la pérdida crece con la rugosidad: a roughness alto, un modelo de un solo rebote puede perder más de la mitad de la energía.
El síntoma visual es inconfundible: los metales rugosos salen demasiado oscuros. Una esfera de aluminio cepillado con roughness a 0.8 se ve gris carbón en vez de gris claro. Como los metales no tienen difuso que compense, se comen la pérdida entera.
En r184, Three.js compensa esa pérdida en los dos frentes, y con dos técnicas distintas:
- Para la luz indirecta,
computeMultiscatteringimplementa el modelo de Fdez-Agüera, que estima la energía de los rebotes múltiples a partir de la integral precalculada de la BRDF y la reinyecta. - Para la luz directa,
BRDF_GGX_Multiscatteraplica la compensación de Turquin, que hace lo mismo evaluando la integral en la dirección de la vista y en la de la luz.
Las dos necesitan la integral direccional de la BRDF, y r184 la lee de una textura de consulta, el uniform dfgLUT, indexada por rugosidad y por el coseno del ángulo de vista. Revisiones anteriores usaban un ajuste polinómico analítico en su lugar. El cambio importa si comparas capturas entre versiones: los materiales rugosos, sobre todo metálicos, se ven algo más claros y más correctos en r184.
Antes de entrar en la BRDF, el shader hace esto:
float geometryRoughness = max( max( dxy.x, dxy.y ), dxy.z );
material.roughness = max( roughnessFactor, 0.0525 );
material.roughness += geometryRoughness;
material.roughness = min( material.roughness, 1.0 );Dos cosas ocurren ahí y las dos son deliberadas. La primera: tu roughness se recorta por abajo a 0.0525, un valor que el propio comentario del código justifica porque “corresponde al mip base de un cubemap de 256”. Aunque escribas roughness: 0, el material nunca será un espejo perfecto, porque un espejo perfecto muestrearía un solo texel del mapa de entorno y produciría aliasing especular brutal en cuanto la superficie tuviera curvatura. La segunda: se suma una rugosidad calculada a partir de las derivadas en pantalla de la normal. Donde la normal cambia mucho de un píxel al de al lado, en un borde curvo o en una malla densa vista de lejos, el material se vuelve automáticamente más rugoso para no producir brillos de un píxel que parpadean al mover la cámara. Es antialiasing especular, va de serie, y explica por qué el mismo material se ve ligeramente más mate en la silueta de un objeto que en el centro.
Qué valores de albedo existen de verdad
La conservación de energía también acota lo que puedes escribir en color. Un albedo es una fracción de luz reflejada, así que no puede valer uno: eso sería un reflector perfecto, que no existe.
Los valores medidos de materiales reales, expresados como valor lineal:
| material | albedo aproximado |
|---|---|
| nieve recién caída | 0.80 a 0.90 |
| pintura blanca | 0.75 a 0.85 |
| papel blanco | 0.70 |
| hormigón seco | 0.30 a 0.35 |
| hierba | 0.20 a 0.25 |
| asfalto nuevo | 0.05 a 0.08 |
| carbón | 0.04 |
Ninguno llega a 0.9 y ninguno baja de 0.02. La guía práctica que usan los equipos de arte, expresada en el valor de ocho bits que verías en un editor de imagen con codificación sRGB, es mantener el color base de un dieléctrico entre 50 y 240 aproximadamente. Un 0xffffff puro como color base no describe ningún material real y, peor, hace que la superficie se coma toda la luz de la escena sin nunca oscurecerse, lo que destruye el contraste de la imagen.
El error simétrico es igual de común: un negro puro 0x000000 es una superficie que absorbe todo, y una escena con materiales negros puros pierde toda la información de forma en las zonas oscuras. Un negro real, como el asfalto o el caucho, está alrededor de 0.05.
Si vienes de trabajar con colores de interfaz, la corrección mental es esta: en PBR el color base no es “cómo quiero que se vea”, es “qué fracción de cada longitud de onda refleja esta materia”. La primera lectura te lleva a ajustar el color hasta que la imagen quede bien con la luz actual, que es exactamente el hábito que el PBR viene a eliminar.