wandres.dev
TSL I · El lenguaje de shading de Three.js

Qué ganas y qué pierdes frente a GLSL

Una comparación honesta: composición, tipado y herramientas contra madurez, documentación y control fino sobre el código emitido.

⏱ 17 min

TSL es mejor que escribir GLSL a mano en casi todo lo que importa para construir aplicaciones, y peor en algunas cosas que importan para depurarlas. Merece la pena tener la lista completa antes de reescribir nada, porque la decisión de migrar no depende de si TSL es bueno sino de qué clase de proyecto tienes.

🎯 Al terminar esta lección sabrás
  • Enumerar las ventajas concretas de TSL sobre GLSL escrito a mano.
  • Enumerar sus desventajas actuales sin edulcorar.
  • Decidir si un proyecto concreto debe migrar y en qué orden.
  • Reconocer qué conocimiento de GLSL sigue siendo necesario.

Lo que ganas

Composición real. Un fragmento de shader se guarda en una variable, se pasa como argumento, se devuelve desde una función, se importa desde otro módulo. Con GLSL como texto, lo más parecido es concatenar cadenas, con sus colisiones de nombres y su fragilidad.

// Un modulo que exporta un efecto reutilizable.
export function velo( base, intensidad ) {
  const franjas = uv().y.mul( 60 ).sin().mul( 0.5 ).add( 0.5 );
  return mix( base, base.mul( 1.6 ), franjas.mul( intensidad ) );
}

Inferencia de tipos. No declaras el tipo de casi nada: cada nodo lo deriva de sus entradas. Los errores de tipo se detectan durante la construcción del shader, con un mensaje que menciona la operación, en lugar de aparecer como un error de compilación de GLSL con un número de línea de un fichero generado.

Sin diccionario de uniformes. La referencia entre el bucle de render y el shader es un objeto de JavaScript, no una cadena emparejada por convención. Un error de nombre pasa de ser un fallo silencioso a ser un error de referencia.

Herramientas del anfitrión. Autocompletado, salto a definición, refactorizaciones, comprobación de tipos con TypeScript, control de versiones con diffs legibles. Todo lo que tu editor sabe hacer con JavaScript funciona sobre TSL, y no funciona sobre una plantilla de cadena.

Dos backends por el precio de uno. El mismo grafo produce GLSL o WGSL según el backend activo, incluida la ruta de respaldo automática cuando WebGPU no está disponible.

Interoperación con los materiales. La ventaja que más cambia el día a día: puedes sustituir una parte de un MeshStandardNodeMaterial sin tocar el resto. El PBR completo, las sombras, la iluminación de entorno y el tone mapping siguen ahí. Es exactamente lo que el nivel treinta y cinco perseguía con cirugía de cadenas, resuelto por diseño.

Composición de efectos analizable. Como el shader es una estructura de datos hasta el último momento, se puede generar con bucles, con configuración, con datos de un editor. Nada de eso es viable con texto.

Lo que pierdes

La densidad de la notación matemática. Esto es real y no se puede maquillar. GLSL tiene operadores; TSL tiene métodos. Una fórmula de física de tres líneas se lee bastante peor:

// GLSL
vec3 h = normalize( l + v );
float d = pow( max( dot( n, h ), 0.0 ), brillo );
// TSL
const h = normalize( l.add( v ) );
const d = pow( max( dot( n, h ), 0 ), brillo );

En este caso la forma funcional aguanta bien, pero en expresiones con paréntesis anidados y prioridades mezcladas la diferencia se nota, y no hay solución: JavaScript no tiene sobrecarga de operadores.

Menos material de referencia. Veinte años de artículos, libros, respuestas y ejemplos de Shadertoy están escritos en GLSL. Traducir cualquiera de ellos a TSL es mecánico pero no automático, y hay que hacerlo a mano.

Una capa más entre tú y el código. Cuando algo va mal, el GLSL o el WGSL que se compila no lo escribiste tú. Depurar exige inspeccionar el código generado, que es legible pero no es tuyo, y correlacionar sus variables con tus nodos. Es el mismo escalón que hay entre depurar JavaScript y depurar el resultado de un transpilador, con la diferencia de que aquí no hay mapas de origen.

Menos control fino. Decisiones que en GLSL tomas tú —dónde poner una variable temporal, cómo desenrollar un bucle, qué precisión pedir— las toma el generador. .toVar() te devuelve una parte de ese control, pero no todo.

Superficie de API grande y joven. Hay muchos nombres, algunos con trampas de orden de argumentos, y la API ha cambiado en versiones recientes: varias funciones están marcadas como obsoletas en r184, entre ellas label() sustituida por setName(), append() sustituida por Stack(), PI2 sustituida por TWO_PI y viewportResolution sustituida por screenSize. Código de tutoriales de hace un año puede usar nombres que ahora avisan o han desaparecido.

Requiere cambiar el punto de entrada. Migrar significa importar de three/webgpu en lugar de three, y decidir qué hacer con el renderer.

Cuándo migrar y en qué orden

Situación Recomendación
Proyecto nuevo con shaders propios TSL desde el principio
Proyecto nuevo sin shaders propios cualquiera de los dos, TSL no estorba
Proyecto existente con un ShaderMaterial aislado migrar cuando toque tocarlo
Proyecto existente con muchas inyecciones onBeforeCompile migrar, es donde más se gana
Proyecto que depende de addons sin equivalente en nodos esperar, comprobar addon por addon
Aplicación en producción y estable no migrar por migrar

Si decides migrar, el orden que menos duele es este. Primero cambia el punto de entrada a three/webgpu conservando el renderer clásico y comprueba que todo sigue igual. Después sustituye los materiales integrados por sus equivalentes de nodos, que aceptan los mismos parámetros de constructor. Después traduce tus shaders propios, de uno en uno, empezando por el más simple. Y solo al final, cuando todo lo demás funcione, plantéate cambiar el renderer.

ℹ️
TSL con el WebGLRenderer clásico requiere un adaptador explícito

En r184 existe un puente para usar materiales de nodos con el WebGLRenderer de siempre. No está activo por defecto: hay que instalarlo con renderer.setNodesHandler( new WebGLNodesHandler() ), importando el adaptador de three/addons/tsl/WebGLNodesHandler.js. Su propio fichero documenta las limitaciones: sin sombras VSM, sin MRT, sin transmisión, sin la pila de post-procesado de WebGPU, sin texturas de almacenamiento. Es un puente de migración, no un destino, y el detalle completo está en el nivel de WebGPURenderer.

Lo que sigue haciendo falta saber

TSL cambia la notación, no los conceptos. Todo lo que has aprendido en los niveles de shaders sigue vigente y sigue siendo necesario.

Sigue habiendo dos etapas, vértice y fragmento, con la interpolación entre ellas. Sigue habiendo paralelismo masivo, y por tanto la ramificación divergente sigue costando. Un If de TSL genera un condicional de shader con las mismas consecuencias que uno de GLSL: si los píxeles de un grupo toman ramas distintas, se ejecutan las dos. Las texturas siguen teniendo mipmaps, filtrado y anisotropía. Las normales siguen necesitando renormalizarse tras interpolarse. El espacio de color sigue importando. Y las funciones se llaman igual: mix, smoothstep, clamp, step, fract, dot, cross y el resto tienen los mismos nombres y la misma semántica que en GLSL.

En realidad la equivalencia es tan directa que se puede tabular entera, y eso es exactamente lo que hace la última lección del nivel siguiente.

La objeción de la notación es real y es la única, y hay una forma de vivir con ella

De todas las pegas de la lista, solo una es intrínseca: la falta de sobrecarga de operadores. Las demás son de madurez y se resolverán solas con el tiempo —más ejemplos, mejores mensajes de error, API estabilizada— pero esta no, porque JavaScript no va a dejar que a + b signifique otra cosa. Y conviene tomársela en serio, porque el sitio donde más duele es precisamente donde el código de gráficos es más delicado: las fórmulas densas de iluminación, las transformaciones de espacios, la trigonometría de una proyección. Ahí d.mul(d).mul(NoH.mul(NoH).mul(a2.sub(1)).add(1)) es sencillamente ilegible, y quien te diga que se acostumbra uno está minimizando un problema real. La estrategia que funciona no es acostumbrarse: es cambiar la granularidad a la que escribes. En GLSL uno tiende a escribir expresiones largas porque los operadores lo permiten y descomponer cuesta líneas. En TSL, descomponer es prácticamente gratis, porque cada paso intermedio es una constante de JavaScript con nombre y .toVar() te asegura que no se recalcula. Así que la misma fórmula se escribe en cinco líneas cortas con nombres —const NoH = dot(n, h), const denom = NoH.mul(NoH).mul(a2.sub(1)).add(1)— y el resultado no solo es legible sino más legible que el GLSL original, porque cada término tiene un nombre en lugar de estar enterrado en un paréntesis. Lo que TSL te quita en densidad te lo devuelve en descomposición barata, si aceptas el cambio de estilo. Quien intenta escribir TSL con la métrica de una línea por fórmula sufre; quien lo escribe como escribiría cualquier otro código JavaScript, con variables intermedias bien nombradas, acaba con shaders que se entienden mejor que los que escribía antes.