Qué es Three.js Shading Language
Qué clase de cosa es TSL exactamente, de dónde se importa, qué relación tiene con GLSL y WGSL, y por qué no es una librería de utilidades.
El nombre engaña un poco. TSL no es un lenguaje en el sentido de tener una gramática, un parser y un compilador propios: es una API de JavaScript que construye un grafo de operaciones, y ese grafo es lo que después se traduce a shaders de verdad. Esa distinción —lenguaje incrustado en lugar de lenguaje aparte— explica casi todo lo demás: cómo se escribe, qué se puede componer, y por qué puede tener dos salidas distintas.
- Describir qué produce una expresión TSL cuando se evalúa en JavaScript.
- Importar TSL y los node materials desde los puntos de entrada correctos.
- Distinguir un lenguaje incrustado de una librería de funciones auxiliares.
- Reconocer los tres nombres que aparecen en todo código TSL.
Un lenguaje que vive dentro de otro
Esta expresión no calcula nada:
import { uv, mix, color } from 'three/tsl';
const gradiente = mix( color( 0x2266ff ), color( 0xff6622 ), uv().x );
No hay ningún píxel involucrado, no hay GPU, no se ha evaluado ninguna interpolación. Lo que gradiente contiene es un objeto: un nodo de tipo mezcla que apunta a dos nodos de color y a un nodo que extrae la componente X de un nodo de coordenadas de textura. Un árbol de cuatro nodos, construido en la CPU, en tiempo de JavaScript.
Cuando ese árbol se asigna a un material y el material se usa para dibujar algo, el motor lo recorre y genera código fuente de shader. Es en ese momento, y no antes, cuando aparece GLSL o WGSL.
La técnica tiene nombre en la literatura: es un lenguaje específico de dominio incrustado, y es el mismo patrón que usan las librerías de consultas SQL tipadas, los constructores de expresiones regulares y los sistemas de shaders de muchos motores. La idea común es que en lugar de escribir el lenguaje destino como texto, escribes en el lenguaje anfitrión una estructura que lo representa, y ganas todo lo que el anfitrión te da: composición, funciones, tipos, herramientas.
La diferencia con una simple librería de utilidades es que TSL no se ejecuta en el mismo sitio que el código que la construye. Una función auxiliar que devuelve un número calcula en la CPU y devuelve un número. mix() no devuelve un color: devuelve la promesa de una mezcla que ocurrirá en la GPU, millones de veces por frame, con valores que ahora mismo no existen.
De dónde se importa cada cosa
Los puntos de entrada son dos, y confundirlos es el primer obstáculo. En el package.json de three r184 el mapa de exportaciones es literalmente:
"exports": {
".": { "import": "./build/three.module.js", "require": "./build/three.cjs" },
"./addons": "./examples/jsm/Addons.js",
"./addons/*": "./examples/jsm/*",
"./webgpu": "./build/three.webgpu.js",
"./tsl": "./build/three.tsl.js"
}
Es decir:
// Las funciones y nodos del lenguaje.
import { uv, mix, color, Fn, uniform, positionLocal } from 'three/tsl';
// Los materiales de nodos, el renderer, y el resto del nucleo.
import * as THREE from 'three/webgpu';
import { MeshStandardNodeMaterial, WebGPURenderer } from 'three/webgpu';
Hay un detalle que ahorra confusión: three/tsl es un envoltorio delgado sobre three/webgpu. Su fuente empieza literalmente con import { TSL } from 'three/webgpu' y sigue con una lista plana de reexportaciones. No son dos librerías: es la misma, con dos superficies distintas. Por eso importar de las dos en el mismo fichero no duplica nada.
Y una consecuencia práctica que sorprende a casi todo el mundo: los node materials no están en three. import { MeshStandardNodeMaterial } from 'three' no funciona. src/Three.js exporta el núcleo, WebGLRenderer y los extras de WebGL; los materiales de nodos y WebGPURenderer viven exclusivamente en three/webgpu. Migrar a TSL implica, como mínimo, cambiar el punto de entrada de tu importación principal.
El punto de entrada three/webgpu incluye todo el núcleo de Three.js: Scene, Mesh, PerspectiveCamera, BufferGeometry, los cargadores, las matemáticas. Lo único que no trae es WebGLRenderer, que sigue en three. Un proyecto puede importar la escena de three/webgpu, el renderer clásico de three, y funcionar. Cómo se conectan los dos es el tema del nivel de WebGPURenderer.
Los tres nombres que verás siempre
Casi cualquier fragmento de TSL que encuentres usa alguna de estas tres cosas, así que conviene reconocerlas desde el principio aunque su detalle venga después.
Constantes de acceso. Nombres que representan datos que la GPU ya tiene: positionLocal, normalLocal, normalView, positionWorld, cameraPosition, modelViewMatrix, time. Se usan directamente, sin paréntesis, porque son valores, no funciones.
material.positionNode = positionLocal.mul( 1.2 );
Funciones constructoras y operaciones. Nombres que se invocan: uv(), color(), vec3(), float(), uniform(), attribute(), texture(), mix(), smoothstep(), sin().
const c = mix( color( 0x000000 ), color( 0xffffff ), uv().y );
Encadenamiento de métodos. Casi toda operación existe también como método sobre cualquier nodo, lo que permite leer de izquierda a derecha:
// Estas dos lineas construyen el mismo grafo.
const a = mul( add( positionLocal.x, 1 ), 0.5 );
const b = positionLocal.x.add( 1 ).mul( 0.5 );
Esa dualidad es deliberada y viene de que Three.js registra cada operación tanto como función suelta como método del prototipo de Node. Cuál usar es cuestión de legibilidad; el grafo resultante es idéntico.
Y un cuarto elemento que aparece en cuanto el código crece:
Fn(), que empaqueta un trozo de grafo en algo reutilizable con parámetros, y que es el equivalente de una función de GLSL.
import { Fn, float } from 'three/tsl';
const ondas = Fn( ( [ p, frecuencia ] ) => {
return p.mul( frecuencia ).sin().mul( 0.5 ).add( 0.5 );
} );
Fíjate en la firma: el callback recibe un array que se desestructura. Es una peculiaridad de la API que confunde la primera vez y que tiene su razón, y la verás con detalle en el nivel siguiente.
Es fácil evaluar TSL como si fuera una cuestión de gusto sintáctico: unos prefieren a.add(b).mul(c) y otros prefieren (a + b) * c, y visto así GLSL gana por goleada, porque tiene operadores de verdad. Ese marco se pierde lo único importante. En GLSL, el shader es una cadena de texto, y una cadena de texto es opaca: no se puede inspeccionar, no se puede componer sin concatenar, no se puede analizar sin escribir un parser, y no se puede transformar sin las cirugías del nivel treinta y cinco. Todo lo que quieras hacer con un shader escrito como texto —variarlo según el material, insertarle un trozo, quitarle otro, generarlo a partir de datos— acaba siendo manipulación de cadenas, con sus expresiones regulares y su fragilidad. En TSL, el shader es una estructura de datos viva hasta el instante mismo de la compilación. Y eso desbloquea una categoría entera de cosas que en GLSL sencillamente no se hacen: montar el grafo con un bucle de JavaScript sobre una lista de capas, guardar un subgrafo en una variable y reutilizarlo en cinco materiales distintos, escribir una función que devuelva un nodo según un parámetro de configuración, recorrer el árbol para contar texturas o estimar coste, construir el material desde un editor visual. El giro conceptual es que has dejado de escribir shaders y has empezado a escribir programas que producen shaders, y ese cambio de nivel es el mismo que hubo entre escribir HTML a mano y generarlo con plantillas. La compilación a dos lenguajes distintos, que es lo que más llama la atención de TSL y lo que le da nombre, resulta ser solo el primer beneficio de haber hecho ese cambio; no es la razón por la que TSL existe, es una consecuencia de ella.