Tabla de equivalencias con GLSL
La traducción completa de tipos, operadores, funciones integradas, constantes y accesores, para convertir cualquier GLSL que ya conozcas.
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.
- 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.
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;
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.