wrapS, wrapT, repeat, offset y rotation: la matriz UV
Los tres modos de repetición, las cuatro propiedades que componen la transformación de textura, y el orden en que se aplican.
Cuatro propiedades de Texture no tocan la imagen: transforman las coordenadas antes de muestrearla. Se combinan en una matriz de tres por tres que el renderer sube como uniform, y saber en qué orden se componen evita la media hora clásica de girar una textura y descubrir que además se ha desplazado.
- Elegir el modo de repetición correcto en cada eje y saber cuál es el defecto.
- Componer
repeat,offset,rotationycentersabiendo el orden de aplicación. - Reconocer el artefacto de borde que produce el modo de repetición equivocado.
- Actualizar la matriz a mano cuando desactives la actualización automática.
Los tres modos de repetición
wrapS controla qué ocurre cuando la coordenada u sale del rango de cero a uno, y wrapT hace lo mismo con v. Los tres valores posibles:
ClampToEdgeWrapping recorta: cualquier valor por debajo de cero muestrea el primer téxel y cualquiera por encima de uno muestrea el último. El resultado es que los píxeles del borde se estiran hacia el infinito. Es el valor por defecto de las dos propiedades.
RepeatWrapping toma la parte fraccionaria: la textura se repite indefinidamente en mosaico.
MirroredRepeatWrapping repite alternando el sentido en cada copia, de forma que los bordes de dos copias contiguas coinciden. Es lo que quieres cuando la textura no es continua en sus bordes, porque elimina la línea de discontinuidad a cambio de introducir un patrón especular reconocible.
Que el defecto sea ClampToEdgeWrapping es la causa del error más frecuente de esta lección: pones texture.repeat.set(4, 4) esperando cuatro baldosas y obtienes una sola baldosa en la esquina y el resto de la superficie pintada con el color de los bordes estirados. La documentación lo advierte: si repeat es mayor que uno en un eje, el modo correspondiente tiene que ser RepeatWrapping o MirroredRepeatWrapping.
import * as THREE from 'three';
const suelo = new THREE.TextureLoader().load('/texturas/parquet.jpg');
suelo.colorSpace = THREE.SRGBColorSpace;
suelo.wrapS = THREE.RepeatWrapping; // obligatorio antes de repetir
suelo.wrapT = THREE.RepeatWrapping;
suelo.repeat.set(8, 8);
La transformación completa
Cuatro propiedades componen la matriz UV:
offset, unVector2, desplaza. Por defecto(0,0).repeat, unVector2, escala. Por defecto(1,1).rotation, un número en radianes, gira en sentido antihorario. Por defecto0.center, unVector2, es el punto alrededor del cual se gira y se escala. Por defecto(0,0), la esquina inferior izquierda.
El método que las combina es updateMatrix, y su implementación deja claro el orden:
updateMatrix() {
this.matrix.setUvTransform(
this.offset.x, this.offset.y,
this.repeat.x, this.repeat.y,
this.rotation,
this.center.x, this.center.y
);
}
La composición es: trasladar al centro, escalar por repeat, rotar, volver del centro, y por último aplicar offset. La consecuencia práctica más importante es que center vale (0,0) por defecto, así que una rotación sin tocar center gira alrededor de la esquina, no del medio. Si querías girar la textura sobre sí misma, la línea que falta es siempre la misma:
textura.center.set(0.5, 0.5);
textura.rotation = Math.PI / 4;
La segunda consecuencia es que offset se aplica después de la escala, así que un offset de 0.5 desplaza medio ancho de la textura original, no medio ancho de una repetición. Con repeat a 4, ese mismo offset de 0.5 mueve la textura dos repeticiones completas.
Esa combinación de repeat y offset es exactamente lo que necesita un atlas o una hoja de sprites. Para una rejilla de cuatro por cuatro celdas:
function seleccionarCelda(textura, columna, fila, columnas = 4, filas = 4) {
textura.repeat.set(1 / columnas, 1 / filas);
// El origen de v esta abajo, asi que la fila 0 es la de arriba.
textura.offset.set(columna / columnas, 1 - (fila + 1) / filas);
}
Ese 1 - (fila + 1) / filas es la consecuencia directa del origen abajo a la izquierda, y es donde se equivoca todo el mundo la primera vez.
Cuándo se recalcula la matriz
matrixAutoUpdate vale true por defecto, y con él el renderer llama a updateMatrix antes de cada dibujado, así que cualquier cambio en las cuatro propiedades tiene efecto inmediato sin hacer nada más. Esa comodidad cuesta una construcción de matriz por textura y por objeto en cada fotograma, que es despreciable salvo en escenas con cientos de materiales.
Si necesitas escribir la matriz directamente, por ejemplo para una transformación que no se puede expresar con esas cuatro propiedades, pon matrixAutoUpdate a false y rellena texture.matrix tú. A partir de ese momento las cuatro propiedades dejan de tener efecto, y es un buen sitio donde perder una tarde si te olvidas.
La trampa está en cómo Three.js sube esas matrices. Cada mapa del material tiene su uniform de transformación propio: mapTransform, normalMapTransform, aoMapTransform, y así con todos. Así que puedes darle a la textura de color un repeat de ocho y a la de normales un repeat de dos, y cada una respeta el suyo.
La excepción es PointsMaterial. En un Points, map y alphaMap comparten el mismo uniform, uvTransform, así que la transformación de la primera se aplica a las dos. Si intentas que la máscara alfa de tus partículas tenga una repetición distinta de la del color, no vas a conseguirlo tocando las texturas.
Hay un segundo detalle que muerde en producción: si dos materiales comparten la misma instancia de textura, comparten también su repeat y su offset, porque esas propiedades viven en la textura y no en el material. Cambiar el repeat para el suelo cambia el de la pared que usaba la misma imagen. La solución no es cargar la imagen dos veces sino clonar la textura con texture.clone(): el clon comparte el objeto Source, es decir, los píxeles y la subida a la GPU, pero tiene su propia matriz de transformación. Es la diferencia entre gastar memoria de vídeo dos veces y no gastarla.
El artefacto del borde
Un último detalle que explica un problema muy común. Con RepeatWrapping y filtrado lineal, el téxel del borde derecho se mezcla con el del borde izquierdo, que es lo correcto porque la textura se repite. Con ClampToEdgeWrapping, no.
Eso significa que una textura pensada para repetirse tiene que ser continua en sus cuatro bordes, y si no lo es, verás una rejilla de líneas donde se juntan las copias. Y al revés: una textura pensada para no repetirse, como una etiqueta o un logotipo, tiene que usar ClampToEdgeWrapping, porque con RepeatWrapping el filtrado del borde izquierdo se contamina con el derecho y aparece un halo de un píxel en los cuatro lados. Ese halo es casi invisible en el nivel base y muy visible en los mipmaps pequeños, así que se manifiesta solo cuando el objeto se aleja, que es cuando menos se espera.