wandres.dev
TSL III · Node materials

Los nodos de utilidad integrados

El catálogo de ruido, patrones, mapeos de UV, ajustes de color y modos de mezcla que Three.js ya trae, con sus firmas reales.

⏱ 18 min

Una parte considerable de lo que uno acaba escribiendo a mano en GLSL ya viene resuelto en TSL, y con implementaciones mejores que las que salen de copiar el primer resultado de una búsqueda. Ruido de varias familias, patrones, proyecciones triplanares, ajustes de color y modos de mezcla están en la exportación de three/tsl esperando a que alguien se dé cuenta.

🎯 Al terminar esta lección sabrás
  • Usar las familias de ruido de MaterialX con sus firmas reales.
  • Aplicar una textura por proyección triplanar sin UV.
  • Ajustar color con los nodos de saturación, tono y vibración.
  • Localizar los nodos de pantalla, tiempo y osciladores.

Ruido

Three.js trae la biblioteca de ruido de MaterialX, que es de calidad de producción y está probada. Los nombres siguen la convención del estándar:

import {
  mx_noise_float, mx_noise_vec3, mx_noise_vec4,
  mx_fractal_noise_float, mx_fractal_noise_vec2, mx_fractal_noise_vec3, mx_fractal_noise_vec4,
  mx_worley_noise_float, mx_worley_noise_vec2, mx_worley_noise_vec3,
  mx_cell_noise_float
} from 'three/tsl';

Las firmas que importan:

mx_noise_float( texcoord = uv(), amplitude = 1, pivot = 0 )
mx_fractal_noise_vec3( position = uv(), octaves = 3, lacunarity = 2, diminish = 0.5, amplitude = 1 )

El ruido fractal es el que se usa para casi todo lo orgánico: el número de octavas controla el detalle, la lacunaridad cuánto sube la frecuencia entre octavas, y el factor de disminución cuánto baja la amplitud.

import { mx_fractal_noise_float, positionLocal, mix, color } from 'three/tsl';

const veta = mx_fractal_noise_float( positionLocal.mul( 3 ), 5, 2, 0.55 )
  .mul( 0.5 ).add( 0.5 )
  .toVar( 'veta' );

material.colorNode = mix( color( 0xe8e2d6 ), color( 0x6b5f4b ), veta );

El ruido de Worley, también llamado celular, da patrones de celdas y sirve para escamas, piedra, burbujas y craquelado. mx_cell_noise_float da un valor constante por celda, útil para variación aleatoria discreta.

Un aviso: mx_noise_vec2 no existe. Está comentado en el código fuente. Las variantes disponibles de mx_noise son float, vec3 y vec4; las de mx_fractal_noise sí incluyen vec2.

Y hay tres nodos de ruido más ligeros para casos simples:

import { rand, hash, triNoise3D, checker } from 'three/tsl';

const aleatorio = rand( uv() );          // pseudoaleatorio determinista desde un vec2
const tablero  = checker( uv().mul( 8 ) ); // patron de damas

checker( coord = uv() ) es un damero clásico y es sorprendentemente útil como textura de depuración: aplicado a un modelo, muestra de un vistazo si sus UV están bien distribuidas.

Mapeos de coordenadas

Triplanar. Aplicar una textura a una geometría sin UV, proyectándola desde los tres ejes y mezclando según la normal. Es la solución estándar para terrenos, rocas y geometría procedural.

import { triplanarTexture, texture, float } from 'three/tsl';

material.colorNode = triplanarTexture( texture( miTextura ), null, null, float( 0.5 ) );

La firma completa, del código fuente:

triplanarTextures( textureXNode, textureYNode = null, textureZNode = null,
                   scaleNode = float( 1 ), positionNode = positionLocal, normalNode = normalLocal )

triplanarTexture es un alias que delega en triplanarTextures. Si solo das la primera textura, se usa para los tres ejes; si das las tres, cada eje usa la suya, que es lo que permite tener por ejemplo hierba arriba y roca en los laterales.

Matcap. Las coordenadas para un material de captura de material se obtienen de un nodo ya evaluado:

import { matcapUV, texture } from 'three/tsl';

material.colorNode = texture( miMatcap, matcapUV );

Ojo: matcapUV es un valor, no una función. No lleva paréntesis.

Equirectangular. Para muestrear un panorama esférico a partir de una dirección:

import { equirectUV, texture, positionWorldDirection } from 'three/tsl';

material.colorNode = texture( miPanorama, equirectUV( positionWorldDirection ) );

Su parámetro tiene por defecto positionWorldDirection, así que equirectUV() sin argumentos ya hace lo habitual.

Rotación. rotate( position, rotation ) gira un vector, con dos argumentos.

Y hay más en el mismo territorio: spritesheetUV para animación por hojas de sprites, pointUV para las coordenadas dentro de un punto, rotateUV para girar coordenadas de textura.

Color: ajustes y mezcla

import { saturation, vibrance, hue, luminance, grayscale, posterize } from 'three/tsl';

Las tres primeras comparten firma: ( color, adjustment = float( 1 ) ), donde uno significa sin cambio.

import { output, saturation, vec4 } from 'three/tsl';

// Baja la saturacion a la mitad, conservando el resto del calculo.
material.outputNode = vec4( saturation( output.rgb, 0.5 ), output.a );

luminance es distinta: es una función normal de JavaScript con argumentos posicionales, luminance( color, coeficientes ), y sus coeficientes por defecto salen del sistema de gestión de color de Three.js, así que respetan el espacio de trabajo activo. Usarla es preferible a escribir a mano el producto escalar con los coeficientes de siempre.

Modos de mezcla

Los cinco modos clásicos de composición, todos con firma ( base, blend ):

import { blendBurn, blendDodge, blendScreen, blendOverlay, blendColor } from 'three/tsl';

material.colorNode = blendOverlay( texturaBase, texturaDetalle );

Los cuatro primeros trabajan con vec3; blendColor trabaja con vec4 y respeta el alfa, que lo hace el adecuado para superponer una capa con transparencia.

Pantalla, tiempo y osciladores

Nodo Qué da
screenUV coordenada normalizada de pantalla, de 0 a 1
screenCoordinate coordenada en píxeles
screenSize tamaño del framebuffer en píxeles
screenDPR la densidad de píxeles
viewportUV, viewportSize, viewportCoordinate equivalentes referidos al viewport
time segundos transcurridos
deltaTime segundos del último frame
frameId número de frame, tipo entero sin signo

Y los cuatro osciladores, todos con la misma firma ( t = time ), que devuelven valores en el rango de cero a uno con periodo uno:

import { oscSine, oscSquare, oscTriangle, oscSawtooth } from 'three/tsl';

material.emissiveNode = color( 0x33ffcc ).mul( oscSine() );
material.emissiveNode = color( 0xff2244 ).mul( oscSquare( time.mul( 4 ) ) );

Remapeo y rangos

import { remap, remapClamp, range } from 'three/tsl';

// De un rango a otro.
const v = remap( ruido, -1, 1, 0.2, 0.8 );

// Igual, pero recortando fuera del rango de entrada.
const w = remapClamp( altura, 0, 10, 0, 1 );

range( min, max ) es distinto y muy útil con instancias: genera un valor aleatorio pero estable por instancia dentro del rango dado, lo que permite variar color, tamaño o velocidad entre miles de copias sin ningún atributo adicional.

import { range, color, mix } from 'three/tsl';

// Cada instancia recibe su propio tono.
material.colorNode = mix( color( 0x2244ff ), color( 0xffaa22 ), range( 0, 1 ) );
💡
Antes de escribir un nodo, busca si ya existe

La superficie de three/tsl es muy grande y está poco documentada por fuera, así que la vía práctica es abrir la lista de exportaciones del paquete desde tu editor y buscar por palabra clave. Los prefijos ayudan: mx_ es la biblioteca de MaterialX, material son los accesores del material, blend son los modos de mezcla, osc los osciladores, screen y viewport las coordenadas de pantalla.

Adoptar MaterialX no fue añadir funciones, fue adoptar un contrato de compatibilidad

La presencia de la biblioteca mx_ en Three.js parece un detalle de conveniencia —alguien portó unas funciones de ruido decentes— y es una decisión con bastante más fondo. MaterialX es un estándar abierto de descripción de materiales nacido en Lucasfilm y adoptado por buena parte de la industria del cine y de los motores en tiempo real. Su valor no está en las funciones sino en que define exactamente qué debe devolver cada una. Cuando usas mx_fractal_noise_vec3 con unos parámetros dados, obtienes el mismo campo de ruido que obtendría un artista en su herramienta de creación de contenido y que obtendría un motor de renderizado offline. Y eso resuelve un problema muy caro que aparece en cuanto un proyecto tiene artistas: el material que se ve en la herramienta y el que se ve en el navegador dejan de ser dos cosas distintas. Cualquiera que haya trabajado en un pipeline sabe cuánto tiempo se va en esa brecha: el artista ajusta unos valores, se exportan, y en el motor se ven diferentes porque la implementación del ruido no es la misma, porque los rangos no coinciden, porque las octavas se cuentan de otra forma. Cada uno de esos desajustes se resuelve a mano, una vez por material, para siempre. Que Three.js hable el mismo dialecto que el resto del sector convierte esos desajustes en una clase de problema que simplemente no ocurre. La lección transferible, que vale para elegir cualquier dependencia, es que una implementación te da un resultado y un estándar te da un acuerdo, y que en cuanto hay más de una herramienta en la cadena, el acuerdo vale mucho más que el resultado. Es exactamente la misma razón por la que glTF ganó a los formatos propietarios.