uniform, attribute y varying
Las tres entradas de datos de un shader en su versión de nodos: cómo se declaran, cómo se actualizan y cómo se cruza la frontera entre vértice y fragmento.
uniform
import { uniform } from 'three/tsl';const uIntensidad = uniform( 1.0 );const uColor = uniform( new THREE.Color( 0xff6622 ) );const uCentro = uniform( new THREE.Vector3() );La firma es uniform( value, type ), con el tipo opcional: se infiere del valor. Los tipos que puede inferir son los habituales de Three.js —número, Color, Vector2, Vector3, Vector4, Matrix3, Matrix4— y hay una forma especial que declara solo el tipo sin valor inicial, uniform( 'float' ).
Actualizarlo es escribir en .value:
uIntensidad.value = 0.4;uColor.value.set( 0x33ff88 );uCentro.value.copy( malla.position );Para valores que dependen del frame o del objeto hay dos ganchos que evitan tener que acordarse de actualizarlos:
// Se ejecuta una vez por frame.const uLatido = uniform( 0 ).onRenderUpdate( ( frame ) => Math.sin( frame.time * 3 ) * 0.5 + 0.5 );// Se ejecuta una vez por objeto que use este material.const uAltura = uniform( 0 ).onObjectUpdate( ( { object } ) => object.position.y );Si el callback devuelve un valor, se asigna a .value automáticamente. Es exactamente el mecanismo con el que Three.js implementa el nodo time, que no es más que un uniforme con un onRenderUpdate que lee el reloj del frame.
Para dar nombre al uniforme en el código generado, y facilitar así la lectura del shader al depurar:
const uVelocidad = uniform( 1.0 ).setName( 'velocidad' );En r184 label() sigue existiendo pero avisa: fue sustituida por setName() en r179.
uniformArray
Para colecciones hay un nodo específico, con acceso por índice:
import { uniformArray } from 'three/tsl';const uPaleta = uniformArray( [ new THREE.Color( 0x1f4fd8 ), new THREE.Color( 0xff7a3d ), new THREE.Color( 0x33ff88 )] );// Acceso con un indice, que puede ser un nodo.const c = uPaleta.element( indice );Los elementos del array que le pasas siguen siendo los mismos objetos, así que mutarlos actualiza el uniforme: uPaleta.array[ 1 ].set( 0x00ffcc ) cambia el segundo color sin reconstruir nada. Es la forma correcta de pasar una paleta o una lista de posiciones de influencia sin gastar un uniforme por elemento.
attribute
Los atributos de vértice se leen por nombre, igual que en GLSL, pero declarando el tipo si el motor no lo conoce:
import { attribute } from 'three/tsl';const aSemilla = attribute( 'semilla', 'float' );const aColor = attribute( 'color', 'vec3' );Y del lado de la geometría, nada cambia respecto a lo que ya sabes:
const geometria = new THREE.SphereGeometry( 1, 64, 64 );const n = geometria.attributes.position.count;const semillas = new Float32Array( n );for ( let i = 0; i < n; i ++ ) semillas[ i ] = Math.random();geometria.setAttribute( 'semilla', new THREE.BufferAttribute( semillas, 1 ) );Los atributos estándar tienen su propio accesor y no hace falta pedirlos por nombre. uv() es literalmente attribute( 'uv', 'vec2' ) con soporte para canales adicionales: uv( 1 ) devuelve uv1, uv( 2 ) devuelve uv2. Y positionGeometry, normalGeometry y compañía cubren el resto.
varying
Un varying es un valor que se calcula en el vértice y llega al fragmento interpolado. En TSL hay tres formas de crearlo, con matices distintos.
import { varying, positionLocal, normalLocal } from 'three/tsl';// Forma funcional, con nombre opcional.const vAltura = varying( positionLocal.y, 'vAltura' );// Forma encadenada, equivalente.const vAltura2 = positionLocal.y.toVarying( 'vAltura' );// Forzar el calculo a la etapa de vertice sin nombrarlo.const vNormal = normalLocal.toVertexStage();vertexStage( node ) es un alias funcional de lo mismo: internamente llama a varying( node ).
La razón de forzar un cálculo al vértice no es solo obtener el valor en el fragmento: es rendimiento. Una malla de diez mil vértices dibujada a pantalla completa tiene del orden de dos millones de fragmentos. Todo lo que se pueda calcular por vértice cuesta doscientas veces menos que calcularlo por fragmento. El compromiso es la precisión: la interpolación lineal de un valor no lineal introduce error, que se nota más cuanto más grandes son los triángulos.
import { mx_fractal_noise_float } from 'three/tsl';// Caro: el ruido se evalua una vez por fragmento.material.colorNode = mix( color( 0x101820 ), color( 0xffffff ), mx_fractal_noise_float( positionLocal.mul( 3 ), 5 ));// Barato: se evalua por vertice y llega interpolado.material.colorNode = mix( color( 0x101820 ), color( 0xffffff ), mx_fractal_noise_float( positionLocal.mul( 3 ), 5 ).toVertexStage());property y varyingProperty
Cuando necesitas una variable declarada explícitamente con un tipo y un nombre —normalmente para interoperar con nodos del sistema o para depurar— hay dos constructores más:
import { property, varyingProperty } from 'three/tsl';const tmp = property( 'vec3', 'miTemporal' );const vDato = varyingProperty( 'float', 'vDato' );Son de uso menos frecuente que los tres anteriores y aparecen sobre todo dentro de la propia biblioteca.