wandres.dev
TSL II · Nodos y composición

Tabla de equivalencias con GLSL

La traducción completa de tipos, operadores, funciones integradas, constantes y accesores, para convertir cualquier GLSL que ya conozcas.

⏱ 16 min

Veinte años de shaders publicados están escritos en GLSL, y traducirlos es una tarea mecánica en cuanto tienes la correspondencia delante. Casi todo se llama igual; lo que cambia son los operadores, los accesores del sistema y un puñado de nombres que TSL escribe en camelCase. Esta lección es la tabla que conviene tener a mano las primeras semanas.

🎯 Al terminar esta lección sabrás
  • Traducir tipos, constructores y swizzling de GLSL a TSL.
  • Traducir operadores aritméticos, de comparación, lógicos y de bits.
  • Localizar el nodo equivalente a cada variable integrada de un shader de Three.js.
  • Identificar los nombres que cambian de ortografía y las funciones que no existen.

Tipos y construcción

GLSL TSL Nota
float x = 1.0; float( 1 ) también const x = float( 1 ).toVar()
int, uint, bool int(), uint(), bool()
vec2, vec3, vec4 vec2(), vec3(), vec4()
ivec3, uvec3, bvec3 ivec3(), uvec3(), bvec3() y sus versiones de 2 y 4
mat2, mat3, mat4 mat2(), mat3(), mat4()
sin equivalente color( 0xff8800 ) atajo para colores, admite hexadecimal
float y = x; (conversión) x.toFloat() también toVec3(), toMat4(), toInt()

El swizzling funciona como propiedades del nodo y admite las tres nomenclaturas de GLSL:

p.x        p.xy      p.xyz     p.wzyx
c.r        c.rgb     c.bgr
t.s        t.stp

Y hay dos familias de métodos que GLSL no tiene:

p.setX( 1 );        // devuelve un nodo con la componente sustituida
p.setXY( vec2() );  // idem para varias componentes
p.flipY();          // invierte el signo de una componente

Para acceder a elementos de un array hay índice directo, de 0 a 31, y el método general:

const a = miArray[ 3 ];
const b = miArray.element( indiceNodo );   // el indice puede ser un nodo

Operadores

GLSL TSL funcional TSL encadenado
a + b add( a, b ) a.add( b )
a - b sub( a, b ) a.sub( b )
a * b mul( a, b ) a.mul( b )
a / b div( a, b ) a.div( b )
a % b mod( a, b ) a.mod( b )
-a negate( a ) a.negate()
a += b a.addAssign( b )
a *= b a.mulAssign( b )
a == b equal( a, b ) a.equal( b )
a != b notEqual( a, b ) a.notEqual( b )
a < b lessThan( a, b ) a.lessThan( b )
a > b greaterThan( a, b ) a.greaterThan( b )
a <= b lessThanEqual( a, b ) a.lessThanEqual( b )
a >= b greaterThanEqual( a, b ) a.greaterThanEqual( b )
a && b and( a, b ) a.and( b )
a || b or( a, b ) a.or( b )
!a not( a ) a.not()
a & b bitAnd( a, b ) a.bitAnd( b )
a | b bitOr( a, b ) a.bitOr( b )
a ^ b bitXor( a, b ) a.bitXor( b )
~a bitNot( a ) a.bitNot()
a << b shiftLeft( a, b ) a.shiftLeft( b )
a >> b shiftRight( a, b ) a.shiftRight( b )
a ? b : c a.mix( b, c )

add, sub, mul, div, and y or aceptan más de dos argumentos.

Funciones integradas

Casi todas conservan el nombre exacto de GLSL. Estas son las que no:

GLSL TSL
inversesqrt inverseSqrt
faceforward faceForward
dFdx, dFdy dFdx, dFdy (igual, pero solo en fragmento)

Los alias en minúscula inversesqrt y faceforward existen como función, pero no están registrados como métodos encadenados.

Y estas existen en TSL sin equivalente directo en GLSL:

TSL Equivale a
oneMinus( x ) 1.0 - x
reciprocal( x ) 1.0 / x
saturate( x ) clamp( x, 0.0, 1.0 )
lengthSq( v ) dot( v, v )
pow2, pow3, pow4 x*x, x*x*x, x*x*x*x
cbrt( x ) raíz cúbica con signo
difference( a, b ) abs( a - b )
rand( uv ) ruido pseudoaleatorio determinista
mixElement, smoothstepElement, stepElement las versiones con el orden invertido que usan los métodos

El resto se llaman igual: abs, sign, floor, ceil, round, trunc, fract, mod, min, max, clamp, mix, step, smoothstep, length, distance, dot, cross, normalize, reflect, refract, pow, exp, exp2, log, log2, sqrt, sin, cos, tan, asin, acos, atan, sinh, cosh, tanh, asinh, acosh, atanh, radians, degrees, all, any, fwidth, transpose, determinant, inverse.

atan acepta uno o dos argumentos; con dos hace de atan2, y el generador emite el nombre correcto según el backend.

Constantes

GLSL de Three.js TSL
PI PI
PI2 TWO_PI
PI_HALF HALF_PI
EPSILON EPSILON
sin equivalente INFINITY

PI2 sigue exportándose en TSL pero está marcada como obsoleta desde r181 en favor de TWO_PI.

El entorno del shader

Variables integradas y accesores

Aquí es donde la traducción deja de ser mecánica, porque los nombres cambian y hay más opciones.

GLSL en un shader de Three.js TSL
position positionGeometry o positionLocal
normal normalGeometry o normalLocal
uv uv()
uv1, uv2 uv( 1 ), uv( 2 )
modelMatrix modelWorldMatrix
modelViewMatrix modelViewMatrix
normalMatrix modelNormalMatrix
projectionMatrix cameraProjectionMatrix
viewMatrix cameraViewMatrix
cameraPosition cameraPosition
gl_FragCoord.xy screenCoordinate
gl_VertexID vertexIndex
gl_InstanceID instanceIndex
gl_FrontFacing frontFacing
resolución del viewport screenSize
tiempo (uniforme propio) time

Y hay accesores de posición y normal en varios espacios que en GLSL habría que calcular a mano:

TSL Qué es
positionLocal posición en espacio de objeto, modificable
positionWorld posición en espacio de mundo
positionView posición en espacio de vista
positionViewDirection dirección desde el fragmento a la cámara
normalLocal normal en espacio de objeto
normalView normal en espacio de vista
normalWorld normal en espacio de mundo
normalFlat normal reconstruida por derivadas

positionLocal está definido como positionGeometry convertido en varying, y es el que hay que modificar cuando quieres desplazar vértices. Y normalWorld está definido como normalView.transformDirection( cameraViewMatrix ), que es exactamente el cálculo que harías a mano.

⚠️
Tres nombres han cambiado recientemente

Código TSL de antes de r178 usa transformedNormalView, transformedNormalWorld y transformedClearcoatNormalView. Los tres siguen existiendo en r184 y avisan en consola; sus sustitutos son normalView, normalWorld y clearcoatNormalView. Es el error más frecuente al copiar ejemplos antiguos.

Estructura del shader

GLSL TSL
uniform float x; const x = uniform( 0 )
attribute vec3 a; const a = attribute( 'a', 'vec3' )
varying float v; const v = expr.toVarying( 'v' )
float f( vec3 p ) { ... } const f = Fn( ( [ p ] ) => { ... } )
if / else if / else If( c, fn ).ElseIf( c, fn ).Else( fn )
switch / case / default Switch( v ).Case( n, fn ).Default( fn )
for ( int i = 0; i < n; i++ ) Loop( n, ( { i } ) => { ... } )
break; / continue; Break(); / Continue();
float t = expr; (temporal) const t = expr.toVar()
const float k = 2.0; const k = float( 2 ).toConst()
gl_Position = ... material.positionNode = ...
gl_FragColor = ... material.colorNode = ... u outputNode
discard; .discard() sobre una condición
#define, #ifdef un if normal de JavaScript al construir el grafo

Esa última fila merece una nota: no hay preprocesador y no hace falta. Una variante que en GLSL se resolvería con un #ifdef en TSL se resuelve eligiendo qué nodos montar, con JavaScript corriente, y el shader generado no contiene ninguna rama.

let c = materialColor;
if ( config.tinte ) c = mix( c, color( config.tinte ), 0.3 );
if ( config.franjas ) c = c.mul( uv().y.mul( 80 ).sin().mul( 0.1 ).add( 0.9 ) );
material.colorNode = c;
Traducir GLSL a TSL revela cuánto de tu shader eran andamios

Cuando conviertes un shader real de GLSL a TSL, pasa algo que no esperabas: el resultado suele ser más corto, a veces bastante. Y no es por la sintaxis, que como viste es más verbosa. Es porque un shader de GLSL está lleno de cosas que no son el efecto. Las declaraciones de uniformes, que en TSL son las mismas variables que ya tenías en JavaScript. Las declaraciones de varyings, con su par duplicado en los dos shaders y su nombre que hay que mantener sincronizado. La construcción manual de la matriz normal, de la posición en espacio de mundo, de la dirección de vista, que en TSL son accesores. Las funciones auxiliares copiadas de otro shader porque no había forma de importarlas. Los bloques de #ifdef para las variantes. El emparejamiento de nombres entre el mapa de uniformes y la fuente. Si mides un shader típico, entre un tercio y la mitad de sus líneas son eso, andamiaje que existe para conectar el shader con su entorno, no para calcular nada. En TSL ese andamiaje no tiene dónde vivir, porque el entorno y el shader son el mismo programa. Lo que queda es el efecto en sí, y con eso pasa lo contrario de lo que suele ocurrir en una migración: el código no solo se acorta sino que se hace más fácil de leer, porque toda línea que sobrevive es una línea que hace algo. Y hay un efecto secundario que se nota al cabo de unas semanas: como conectar deja de costar, empiezas a descomponer más. Los efectos que en GLSL habrías escrito como un bloque monolítico porque partirlos implicaba mantener más uniformes y más varyings, en TSL los partes en cinco funciones porque no cuesta nada. La calidad del código sube por una razón puramente económica, que suele ser la única razón por la que la calidad del código sube de verdad.