aoMap y lightMap: por qué exigen un segundo juego de UVs
Qué término de la iluminación multiplica cada uno, qué significa que un desenvolvido no pueda solaparse, y cómo se elige el canal de UV en Three.js.
Los mapas de oclusión ambiental y de luz son los únicos del catálogo que guardan información calculada para un punto concreto de un modelo concreto, y no una propiedad de la materia. Esa diferencia tiene una consecuencia dura: el desenvolvido que sirve para repetir una textura de ladrillo no sirve para ellos, y hace falta un segundo juego de coordenadas construido con otras reglas.
- Explicar qué términos de la iluminación multiplica
aoMapy cuáles deja intactos. - Justificar por qué un desenvolvido con solapamientos invalida estos mapas.
- Configurar el atributo
uv1y la propiedadchannelde la textura. - Distinguir la oclusión ambiental horneada de la calculada en post-proceso.
Qué multiplica exactamente aoMap
La oclusión ambiental estima qué fracción del hemisferio le queda visible a cada punto de la superficie. Un rincón entre dos paredes ve poco cielo, así que recibe menos luz ambiental que una superficie abierta. Es una aproximación barata de un efecto de iluminación global.
En Three.js se aplica así:
float ambientOcclusion = ( texture2D( aoMap, vAoMapUv ).r - 1.0 ) * aoMapIntensity + 1.0;
reflectedLight.indirectDiffuse *= ambientOcclusion;
#if defined( USE_ENVMAP ) && defined( STANDARD )
float dotNV = saturate( dot( geometryNormal, geometryViewDir ) );
reflectedLight.indirectSpecular *= computeSpecularOcclusion( dotNV, ambientOcclusion, material.roughness );
#endif
Tres cosas importantes ahí.
La primera es la fórmula de la intensidad. aoMapIntensity interpola entre uno, sin oclusión, y el valor del mapa. Con intensidad cero el mapa no hace nada; con uno se aplica entero. No es un multiplicador simple, es una mezcla.
La segunda es que la oclusión afecta solo a los términos indirectos. La luz directa de una lámpara no se atenúa, y es correcto: la oclusión ambiental describe el acceso a la luz ambiente, no la sombra proyectada de una fuente concreta. Si oscureciera también la directa, un rincón iluminado de frente por un foco saldría absurdamente oscuro.
La tercera es que el especular indirecto no se multiplica por el mismo valor sino por una oclusión especular calculada aparte, que depende del ángulo de vista y de la rugosidad. La razón física es que una superficie pulida refleja en una dirección concreta y puede perfectamente reflejar algo aunque esté en un rincón, así que ocluirla con el mismo factor que el difuso la oscurecería de más.
Con clearcoat o sheen activos, sus términos indirectos también se multiplican por la oclusión.
Por qué no vale el primer desenvolvido
El desenvolvido normal de un modelo está optimizado para aprovechar la textura. Eso significa dos cosas que aquí son venenosas: las islas se solapan cuando dos partes del modelo pueden compartir la misma región de textura, y las UV salen del rango de cero a uno cuando la textura se repite.
Para un mapa de ladrillo, ambas cosas son ventajas: las cuatro patas idénticas de una mesa pueden compartir téxeles y ahorrar tres cuartas partes de la memoria.
Para un mapa de oclusión, ambas son fatales. La oclusión de la pata delantera izquierda es distinta de la de la trasera derecha, porque están en sitios distintos y ven cosas distintas. Si comparten téxeles, solo puedes guardar un valor y las cuatro patas se verán con la oclusión de una sola. Y con UV fuera del rango, la oclusión se repetiría en mosaico, lo cual no tiene ningún sentido.
De ahí el requisito, que la documentación de Three.js repite en cada material: aoMap y lightMap requieren un segundo juego de UVs. Ese segundo desenvolvido tiene que ser inyectivo, es decir, cada punto del modelo en su propio téxel, sin solapamientos y dentro del rango. En las herramientas de modelado suele llamarse desenvolvido de lightmap y se genera automáticamente.
uv1 y el canal de la textura
Three.js soporta hasta cuatro juegos de coordenadas en los atributos uv, uv1, uv2 y uv3. Cada textura elige el suyo con la propiedad channel, que vale 0 por defecto y se corresponde con uv.
import * as THREE from 'three';
const geometria = new THREE.BoxGeometry(1, 1, 1);
// El segundo juego, aqui simplemente una copia del primero.
// En produccion vendria del desenvolvido de lightmap del modelo.
geometria.setAttribute('uv1', geometria.attributes.uv.clone());
const oclusion = new THREE.TextureLoader().load('/texturas/caja_ao.png');
oclusion.channel = 1; // usa uv1
const material = new THREE.MeshStandardMaterial({
map: color,
aoMap: oclusion,
aoMapIntensity: 1.0
});
La propiedad channel es relativamente reciente en la historia de Three.js. Durante años el segundo juego estaba cableado y el atributo se llamaba uv2, así que hay mucho código y muchos tutoriales que hacen geometry.setAttribute('uv2', ...). Eso ya no funciona como antes: el atributo uv1 es el segundo juego, y la selección se hace con channel.
Con modelos glTF no hay que hacer nada de esto: el formato declara qué juego usa cada textura y GLTFLoader configura channel correctamente. El trabajo manual solo aparece con geometría propia o con texturas que asignas tú a un modelo cargado.
Cuando aoMap no hace nada, o hace algo que no esperabas, la causa suele estar en uno de estos cuatro sitios, y el orden de comprobación importa:
- No hay iluminación indirecta. La oclusión multiplica
indirectDiffuse. Si no hayenvMapniscene.environmentni luz ambiental ni hemisférica, ese término vale cero, y cero por cualquier oclusión sigue siendo cero. Es el caso más frecuente y el más desconcertante, porque el mapa está bien y la configuración también. - Falta el atributo
uv1. Sin él, la textura se muestrea con coordenadas ausentes. aoMapIntensitya cero. Poco frecuente, pero es lo primero que alguien toca al depurar y luego se olvida de devolverlo.- El mapa está marcado como sRGB. Aquí el efecto es el contrario: la decodificación baja los valores, así que la oclusión sale bastante más fuerte y sucia de lo que pintaste, y el objeto parece tener manchas.
El primer punto tiene una consecuencia de diseño que conviene interiorizar: la oclusión ambiental y el mapa de entorno son complementarios obligatorios. Cuanto mejor sea tu iluminación indirecta, más se nota la oclusión; y al revés, cuanta más oclusión hornees, más importante es tener una indirecta decente que ocluir. Una escena con aoMap en todos los objetos y sin entorno tiene toda la información hecha y no la usa.
lightMap y el horneado
lightMap es el paso siguiente: en lugar de guardar cuánta luz ambiental se ocluye, guarda cuánta luz llega, con su color, calculada por adelantado con un algoritmo que sí puede permitirse rebotes múltiples. Se multiplica por la contribución difusa y se escala con lightMapIntensity.
Es la técnica que hace posibles los interiores creíbles en la web: la iluminación global que un motor en tiempo real no puede calcular se precocina en un render offline y viaja como una textura. Comparte con aoMap el requisito del segundo desenvolvido, por la misma razón exacta, y añade uno propio: la escena tiene que ser estática, porque la luz horneada no se entera de que un objeto se ha movido.
Conviene no confundir el aoMap horneado con la oclusión ambiental calculada en pantalla en post-proceso. La horneada conoce la geometría real del objeto entero, incluidos detalles que no están en la malla de baja densidad, y no cuesta nada en tiempo de ejecución más allá de una lectura de textura. La de pantalla solo conoce lo que está en el buffer de profundidad, no ve lo que queda fuera de cámara, y cuesta un pase completo. Se usan juntas, no como alternativas.