Los slots de superficie
colorNode, emissiveNode, roughnessNode, metalnessNode, opacityNode y normalNode: qué sustituye cada uno, con qué tipo y en qué espacio.
Color y emisión
colorNode
Sustituye el color difuso base, el que en el material clásico controla la propiedad color multiplicada por map.
import { color, uv, mix, texture } from 'three/tsl';material.colorNode = color( 0xff6622 );material.colorNode = mix( color( 0x1f4fd8 ), color( 0xff7a3d ), uv().y );material.colorNode = texture( miTextura ).mul( color( 0xffddaa ) );Acepta vec3 o vec4; internamente se convierte a vec4. Si le das cuatro componentes, la cuarta se usa como opacidad y se combina con la del material, así que un vec4 con alfa distinto de uno afecta a la transparencia sin tocar opacityNode.
Y el detalle que más importa: el valor está en espacio lineal. color( 0xff6622 ) hace la conversión desde sRGB por ti, porque el constructor de Color de Three.js respeta el sistema de gestión de color. Pero un valor construido a mano con vec3( 1.0, 0.4, 0.13 ) es lineal directamente, y saldrá más claro de lo que esperas si lo elegiste mirando un selector de color.
emissiveNode
La luz que el material emite por sí mismo. No se ilumina ni recibe sombras: se suma al final.
import { color, oscSine, uv, smoothstep } from 'three/tsl';// Una banda que late.const banda = smoothstep( 0.48, 0.5, uv().y ).mul( smoothstep( 0.52, 0.5, uv().y ) );material.emissiveNode = color( 0x33ffcc ).mul( banda ).mul( oscSine().mul( 0.5 ).add( 0.5 ) );Espera un vec3 en espacio lineal, y puede superar el valor uno. Ese es precisamente su interés: un emisivo de valor cinco es lo que hace que un UnrealBloomPass con umbral alto lo detecte como fuente de luz y lo haga resplandecer, mientras que el resto de la escena permanece limpia. La combinación de emisivo por encima de uno con buffers de media precisión y tone mapping es la receta estándar de los efectos luminosos.
Recuerda que solo MeshStandardNodeMaterial y sus descendientes lo declaran, así que en un material básico hay que asignarlo como propiedad, no en el constructor.
Rugosidad y metalidad
Los dos parámetros del modelo PBR. Ambos esperan un float en el rango de cero a uno.
import { texture, uv, mx_fractal_noise_float, positionLocal, clamp } from 'three/tsl';// Rugosidad desde un canal de textura.material.roughnessNode = texture( mapaORM ).g;// Metalidad procedural: metal en las zonas bajas, dielectrico arriba.material.metalnessNode = clamp( positionLocal.y.negate().mul( 2 ), 0, 1 );// Rugosidad ruidosa, para romper el aspecto de plastico perfecto.material.roughnessNode = mx_fractal_noise_float( positionLocal.mul( 8 ), 3 ) .mul( 0.5 ).add( 0.5 ) .mul( 0.4 ).add( 0.15 );Ese último ejemplo ilustra el patrón habitual: el ruido devuelve un rango centrado en cero, se lleva a cero-uno, y luego se comprime al rango de rugosidad que de verdad quieres, aquí entre 0.15 y 0.55. Dejar la rugosidad recorrer todo el rango casi nunca queda bien.
Sobre metalness conviene recordar la teoría: en el modelo PBR es un interruptor conceptual, no un mando. Los valores intermedios solo tienen sentido como transición espacial entre una zona metálica y otra que no lo es, por ejemplo pintura desconchada sobre acero. Una superficie con metalness a 0.5 uniforme no corresponde a ningún material real.
Opacidad y normal
opacityNode y alphaTestNode
import { uv, smoothstep, length } from 'three/tsl';// Un disco que se desvanece en el borde.material.transparent = true;material.opacityNode = length( uv().sub( 0.5 ) ).smoothstep( 0.5, 0.3 );opacityNode espera un float de cero a uno. Y hay un requisito que no está en el nodo sino en el material: material.transparent = true sigue siendo necesario, porque es lo que decide el orden de dibujado y el modo de mezcla. Un opacityNode sobre un material opaco no hace nada visible.
Para recortes duros, alphaTestNode es más barato que la transparencia porque no requiere ordenar:
material.alphaTestNode = float( 0.5 );material.opacityNode = texture( mascara ).r;normalNode
Sustituye la normal de la superficie. Es el más delicado de los seis, porque el espacio importa.
import { normalMap, texture, normalLocal, normalize } from 'three/tsl';// Desde un mapa de normales en espacio tangente.material.normalNode = normalMap( texture( mapaNormal ) );// Aplanado deliberado: normal geometrica sin mapas.material.normalNode = normalLocal;El nodo normalMap() hace el trabajo de convertir del espacio tangente al espacio de vista, que es lo que el modelo de iluminación espera. Si le das directamente el color de la textura sin pasar por normalMap(), estarás alimentando el modelo con un vector en el rango cero-uno que además está en otro espacio, y el resultado es un sombreado sin sentido que sin embargo no da ningún error.
Hay también bumpMap() para mapas de altura, que deriva la normal de las pendientes de la textura, y normalFlat para obtener la normal geométrica reconstruida por derivadas de pantalla, útil para conseguir un facetado duro sin duplicar vértices.
Componer varios slots desde una máscara
El patrón que hace que todo esto valga la pena es derivar varias propiedades de un mismo cálculo. Un ejemplo de óxido que afecta a color, rugosidad y metalidad a la vez:
import { color, mix, float, positionLocal, mx_fractal_noise_float, smoothstep} from 'three/tsl';// Una unica mascara, calculada una vez y compartida.const oxido = mx_fractal_noise_float( positionLocal.mul( 4 ), 4 ) .mul( 0.5 ).add( 0.5 ) .smoothstep( 0.45, 0.65 ) .toVar( 'oxido' );const material = new THREE.MeshStandardNodeMaterial();material.colorNode = mix( color( 0x8899aa ), color( 0x6b3a1f ), oxido );material.roughnessNode = mix( float( 0.15 ), float( 0.9 ), oxido );material.metalnessNode = mix( float( 1.0 ), float( 0.0 ), oxido );El .toVar( 'oxido' ) no es opcional aquí. Sin él, el ruido fractal de cuatro octavas —que no es barato— podría acabar evaluado tres veces, una por cada slot que lo usa. Con él, se calcula una vez y las tres mezclas leen la misma variable.