Saltar al contenido

Dos backends, dos lenguajes

Qué separa realmente a GLSL de WGSL, por qué mantener el mismo shader en ambos a mano es inviable, y qué problema concreto vino a resolver TSL.

Lo que separa a los dos lenguajes

GLSL ES 3.0 y WGSL hacen lo mismo y no se parecen en casi nada. Las diferencias no son cosméticas: son estructurales, y por eso una traducción automática ingenua no funciona.

La sintaxis viene de familias distintas. GLSL desciende de C. WGSL está inspirado en Rust: declaraciones con let y var, tipos después del nombre, funciones con fn y ->.

GLSL
// GLSL ES 3.0vec3 iluminar( vec3 n, vec3 l ) {  float d = max( dot( n, l ), 0.0 );  return vec3( d );}
WGSL
// WGSLfn iluminar( n: vec3f, l: vec3f ) -> vec3f {  let d = max( dot( n, l ), 0.0 );  return vec3f( d );}

El modelo de recursos es completamente distinto. GLSL tiene uniforms sueltos que se enlazan por nombre; WGSL tiene grupos de enlace y ubicaciones explícitas, con @group y @binding declarados en el shader y emparejados con un layout de pipeline declarado en JavaScript. No hay correspondencia uno a uno: un puñado de uniforms de GLSL se convierte en una estructura empaquetada según reglas de alineación estrictas.

Las texturas se separan del muestreador. En GLSL un sampler2D es una sola cosa. En WGSL son dos: texture_2d<f32> y sampler, declarados por separado y combinados en la llamada. Eso cambia la forma de todas las funciones que muestrean.

Las entradas y salidas se declaran de otra forma. GLSL usa variables globales in y out más gl_Position y gl_FragCoord. WGSL usa estructuras con atributos @location y @builtin, y la función devuelve su salida en lugar de escribir en globales.

Los sistemas de coordenadas no coinciden. Este es el que muerde en silencio. El rango de profundidad de recorte en WebGL va de menos uno a uno; en WebGPU va de cero a uno. Cualquier matemática que toque la profundidad da resultados distintos. Three.js lo modela explícitamente con las constantes WebGLCoordinateSystem y WebGPUCoordinateSystem, y hay código en el generador que ramifica según cuál esté activo.

Algunas funciones se llaman distinto. El caso más citado es la arcotangente de dos argumentos: en GLSL es atan( y, x ) sobrecargando la de un argumento, en WGSL es atan2( y, x ). El generador de nodos de Three.js lo traduce automáticamente, pero es exactamente la clase de diferencia que hace fracasar una traducción por sustitución de texto.

Por qué duplicar no era una opción

Imagina el trabajo real. La biblioteca de shaders de Three.js tiene más de un centenar de chunks y una docena larga de shaders completos, cubriendo materiales, sombras de varios tipos, niebla, morph targets, skinning, instancing, batching, planos de recorte, media docena de operadores de tone mapping y todo el modelo PBR con sus capas de clearcoat, sheen, iridescencia, anisotropía y transmisión.

Duplicar eso en WGSL significa mantener dos copias de cada uno de esos algoritmos. Y mantener dos copias no es escribirlas dos veces: es que cada corrección de un bug, cada nueva característica y cada ajuste de la ecuación de renderizado hay que hacerlo dos veces, en dos lenguajes, con dos conjuntos de pruebas, y verificando que las dos siguen produciendo la misma imagen. Para un proyecto sostenido en buena medida por voluntarios, eso es sencillamente inviable, y el resultado predecible sería que una de las dos copias se quedara atrás.

La segunda opción, abandonar WebGL, era peor por razones de fuera del proyecto. WebGPU no estuvo en los tres motores hasta que Safari 26 lo implementó en septiembre de 2025, y aun hoy la cobertura de dispositivos de WebGL2 sigue siendo mayor. Una librería cuyo valor es funcionar en todas partes no puede permitirse dejar fuera a una parte del parque.

La tercera opción es la que se tomó: describir los algoritmos una sola vez, en una representación independiente del lenguaje destino, y generar cada uno desde ahí. Los shaders del sistema de materiales de nodos están escritos en TSL, no en GLSL ni en WGSL, y las dos salidas se producen a partir de la misma fuente. La biblioteca se mantiene una vez.

Lo que TSL no resuelve

Conviene ser preciso sobre el alcance, porque es fácil sobrevender esto.

TSL abstrae el lenguaje de shading. No abstrae el modelo de ejecución de las dos APIs. Los compute shaders, los storage buffers, los grupos de trabajo y las barreras de sincronización existen en WebGPU y no en WebGL2, y ninguna generación de código los va a hacer aparecer. Un grafo TSL que use storage() y compute funciona sobre el backend de WebGPU y no tiene traducción al de WebGL.

Tampoco abstrae el rendimiento. El mismo grafo genera código para dos pipelines con características distintas, y el resultado puede tener un perfil de rendimiento distinto en cada uno. No es raro que un efecto vaya notablemente mejor en un backend que en el otro.

Y no elimina la necesidad de entender shaders. TSL cambia cómo los escribes, no qué son: sigues necesitando saber qué es un fragmento, por qué la ramificación es cara, qué significa una normal interpolada y por qué el orden de las operaciones importa. Todo lo que has aprendido escribiendo GLSL sigue aplicando; lo único que cambia es la notación y quién genera el texto final.