Saltar al contenido

Reemplazar sin perder el PBR

Los accesores material* que devuelven el valor por defecto de cada slot, cómo modificar en lugar de sustituir, y los slots que operan por encima del modelo de iluminación.

Los accesores material

Recuerda el patrón que usa NodeMaterial internamente:

JS
let colorNode = this.colorNode ? vec4( this.colorNode ) : materialColor;

Ese materialColor de la rama por defecto es un nodo exportado de three/tsl, y puedes usarlo tú. Es decir, si escribes:

JS
import { materialColor, color, mix } from 'three/tsl';material.colorNode = mix( materialColor, color( 0xff0000 ), 0.25 );

estás diciendo “toma lo que ibas a calcular de todas formas y tíñelo un cuarto de rojo”. La textura difusa, el color base y el color de vértice siguen ahí; solo has añadido una capa encima.

MaterialNode exporta treinta y ocho de estos accesores. Los que más se usan:

Accesor Devuelve
materialColor el color difuso resuelto, con map aplicado
materialOpacity la opacidad, con alphaMap aplicado
materialRoughness la rugosidad, con roughnessMap
materialMetalness la metalidad, con metalnessMap
materialEmissive el emisivo, con emissiveMap
materialNormal la normal, con normalMap y bumpMap
materialAO la oclusión, con aoMap
materialAlphaTest el umbral de alpha test
materialIOR el índice de refracción
materialSpecularColor, materialSpecularIntensity el especular
materialClearcoat, materialClearcoatRoughness, materialClearcoatNormal la capa de barniz
materialSheen, materialSheenRoughness el brillo textil
materialIridescence, materialIridescenceIOR, materialIridescenceThickness la iridiscencia
materialTransmission, materialThickness, materialAttenuationColor, materialAttenuationDistance la transmisión
materialAnisotropy, materialAnisotropyVector la anisotropía
materialDispersion la dispersión
materialRotation, materialPointSize sprites y puntos
materialLineScale, materialLineDashSize, materialLineGapSize, materialLineWidth, materialLineDashOffset líneas
materialLightMap el mapa de luz

Y tres más que viven en otro módulo pero se importan igual: materialRefractionRatio, materialEnvIntensity y materialEnvRotation.

Lo importante de estos accesores no es la lista sino lo que significan: incluyen el trabajo de los mapas. materialRoughness no es el número material.roughness, es ese número multiplicado por el canal verde de roughnessMap si lo hay. Componer sobre él conserva todo el flujo de trabajo de texturas.

Un ejemplo de composición

Un material que envejece: mantiene su textura y sus mapas, pero oscurece las cavidades y añade rugosidad donde la geometría es cóncava.

JS
import {  materialColor, materialRoughness, materialAO,  color, mix, float, positionLocal, mx_fractal_noise_float, clamp} from 'three/tsl';const suciedad = mx_fractal_noise_float( positionLocal.mul( 6 ), 4 )  .mul( 0.5 ).add( 0.5 )  .smoothstep( 0.4, 0.75 )  .toVar( 'suciedad' );const material = new THREE.MeshStandardNodeMaterial( {  map: mapaDifuso,  roughnessMap: mapaRugosidad,  aoMap: mapaOclusion,  roughness: 0.6} );// Oscurece sobre lo que ya habia, sin perder la textura.material.colorNode = mix( materialColor, color( 0x2b2118 ), suciedad.mul( 0.7 ) );// Sube la rugosidad partiendo de la que dice el mapa.material.roughnessNode = clamp( materialRoughness.add( suciedad.mul( 0.35 ) ), 0, 1 );

Sin los accesores, ese mismo efecto obligaría a muestrear las tres texturas a mano y a reimplementar cómo se combinan con los escalares del material, que es exactamente el trabajo que el material ya hace bien.

materialReference, para lo que no tiene accesor

Cuando necesitas leer una propiedad del material para la que no hay accesor —incluidas propiedades tuyas— existe un nodo genérico:

JS
import { materialReference } from 'three/tsl';// Una propiedad propia del material.material.miIntensidad = 0.4;const intensidad = materialReference( 'miIntensidad', 'float' );material.emissiveNode = color( 0x33ffcc ).mul( intensidad );

La ventaja sobre un uniform es que el valor pertenece al material, así que si clonas el material o tienes varios que comparten el mismo grafo de nodos, cada uno lleva su valor. Un uniform es compartido por todos los materiales que referencien ese nodo; una referencia de material es por instancia.

Por encima del modelo: outputNode y lightsNode

Los slots de superficie describen entradas del modelo de iluminación. Hay dos que operan a otro nivel.

outputNode sustituye el color final, después de que la iluminación se haya resuelto entera. Es el sitio para efectos que no son propiedades de la superficie: un ajuste de color, un desvanecido, una mezcla con otra cosa.

JS
import { output, saturation, screenUV, mix, vec4 } from 'three/tsl';// Desatura hacia los bordes de la pantalla.const radio = screenUV.sub( 0.5 ).length();material.outputNode = vec4( saturation( output.rgb, radio.oneMinus() ), output.a );

output es el nodo que representa el color ya calculado, y es lo que hace posible modificar en lugar de sustituir, igual que los accesores material* en los slots de superficie.

lightsNode sustituye el conjunto de luces que el material considera. Su uso más habitual no es inventar un modelo de iluminación sino restringir qué luces afectan a qué objetos, que es algo que Three.js no permite de otra forma:

JS
import { lights } from 'three/tsl';// Este material solo se ilumina con estas dos luces.material.lightsNode = lights( [ luzPrincipal, luzRelleno ] );

Y hay dos slots más que rara vez se necesitan pero conviene saber que existen: castShadowNode, que sustituye lo que el material escribe al generar su shadow map, y receivedShadowNode, que recibe el factor de sombra calculado y permite modificarlo antes de que se aplique.

JS
// Sombras teñidas de azul en lugar de simplemente oscuras.material.receivedShadowNode = Fn( ( [ sombra ] ) => {  return sombra.mix( color( 0x1a2a55 ), 1 );} );

Fíjate en el orden de .mix() una vez más: el receptor sombra es el factor, y cuando vale uno el resultado es el segundo argumento. Con sombra completa —valor cero— se devuelve el color azulado.