Operadores y encadenamiento
Las dos formas de escribir cada operación, la lista real de operadores de r184, las variantes de asignación, y la trampa de orden de argumentos que pilla a todo el mundo.
Las dos formas
Three.js registra cada operación en el prototipo de Node. El mecanismo, literal:
Node.prototype[ name ] = function ( ...params ) { return this.isStackNode ? this.addToStack( nodeElement( ...params ) ) : nodeElement( this, ...params );};Léelo con calma porque es la clave de todo lo demás: el receptor del método se pasa como primer argumento a la función correspondiente. Por eso estas dos líneas construyen el mismo grafo:
import { add, mul, positionLocal } from 'three/tsl';const a = mul( add( positionLocal.x, 1 ), 0.5 );const b = positionLocal.x.add( 1 ).mul( 0.5 );Elegir una u otra es cuestión de legibilidad. La forma encadenada gana cuando hay una cadena de transformaciones sobre un mismo valor; la funcional gana cuando la operación tiene varios argumentos igual de importantes, como mix o clamp, o cuando anidar métodos te obligaría a poner el argumento principal en el sitio equivocado.
Los operadores de r184
Todos existen como función y todos están registrados como método.
| Categoría | Nombres |
|---|---|
| Aritméticos | add, sub, mul, div, mod |
| Comparación | equal, notEqual, lessThan, greaterThan, lessThanEqual, greaterThanEqual |
| Lógicos | and, or, not, xor |
| De bits | bitAnd, bitOr, bitXor, bitNot, shiftLeft, shiftRight |
| Incremento | increment, decrement, incrementBefore, decrementBefore |
add, sub, mul, div, and y or aceptan un número variable de argumentos a partir de dos, así que mul( a, b, c, d ) es válido y produce un solo encadenamiento.
import { positionLocal, normalWorld, dot, vec3, and } from 'three/tsl';const arriba = dot( normalWorld, vec3( 0, 1, 0 ) ).greaterThan( 0.7 );const alto = positionLocal.y.greaterThan( 0.5 );const cima = and( arriba, alto );Un aviso: modInt() sigue exportándose pero está marcada como obsoleta desde r175 y avisa en consola. Su sustituto es mod( int( ... ) ).
Las variantes de asignación
Para cada operación registrada, Three.js instala además una variante con sufijo Assign:
Node.prototype[ name + 'Assign' ] = function ( ...params ) { return this.isStackNode ? this.assign( params[ 0 ], nodeElement( ...params ) ) : this.assign( nodeElement( this, ...params ) );};Eso te da .addAssign(), .subAssign(), .mulAssign(), .divAssign(), .modAssign(), .bitAndAssign(), .shiftLeftAssign() y el resto. Equivalen a los operadores compuestos de GLSL:
import { Fn, float, Loop } from 'three/tsl';const acumular = Fn( ( [ n ] ) => { const total = float( 0 ).toVar(); Loop( 8, ( { i } ) => { total.addAssign( n.mul( i ) ); // total += n * i } ); return total;} );Dos condiciones para que esto funcione. La primera: el nodo destino tiene que ser una variable, creada con .toVar(); no se puede asignar a una expresión. La segunda: la asignación tiene que ocurrir dentro de un Fn(), porque el mecanismo necesita una pila de instrucciones donde apuntar la sentencia. Fuera de ella, Three.js emite este error literal:
TSL: No stack defined for assign operation. Make sure the assign is inside a Fn().La trampa del orden de argumentos
Aquí está el detalle que pilla a todo el mundo, incluido quien lleva tiempo con TSL. Tres operaciones matemáticas de tres argumentos tienen un método encadenado cuyo orden no coincide con el de la función. La definición, literal de MathNode.js en r184:
export const mixElement = ( t, e1, e2 ) => mix( e1, e2, t );export const smoothstepElement = ( x, low, high ) => smoothstep( low, high, x );export const stepElement = ( x, edge ) => step( edge, x );addMethodChaining( 'mix', mixElement );addMethodChaining( 'smoothstep', smoothstepElement );addMethodChaining( 'step', stepElement );Combinado con la mecánica del encadenamiento, que pasa el receptor como primer argumento, el resultado es:
| Encadenado | Equivale a | El receptor es |
|---|---|---|
t.mix( a, b ) |
mix( a, b, t ) |
el factor de interpolación |
x.smoothstep( lo, hi ) |
smoothstep( lo, hi, x ) |
el valor de entrada |
x.step( borde ) |
step( borde, x ) |
el valor de entrada |
Para smoothstep y step el diseño es natural: el receptor es el valor que se está transformando, y los argumentos son los bordes.
Para mix es contraintuitivo y es donde está el peligro. Quien viene de GLSL escribe colorA.mix( colorB, factor ) esperando mix( colorA, colorB, factor ), y obtiene mix( colorB, factor, colorA ). Como los tres son colores o vectores compatibles, no hay error de tipo: el shader compila y da un resultado incorrecto.
El uso correcto en la biblioteca de Three.js deja claro cuál es la intención. En ColorAdjustment.js:
export const saturation = /*@__PURE__*/ Fn( ( [ color, adjustment = float( 1 ) ] ) => { return adjustment.mix( luminance( color.rgb ), color.rgb );} );El receptor es adjustment, un escalar de ajuste, y los dos colores van como argumentos. Y en MaterialXNodes.js el patrón se usa como selector booleano:
export const mx_ifgreater = ( value1, value2, in1, in2 ) => value1.greaterThan( value2 ).mix( in1, in2 );El receptor es una condición booleana que hace de factor. Leído así, a.mix( x, y ) significa “usa a para elegir entre x e y”, lo cual es coherente. Pero solo si lo sabes.
Los alias que no están encadenados
Un detalle menor con consecuencias prácticas: MathNode.js exporta dos alias en minúscula para compatibilidad con la ortografía de GLSL, faceforward e inversesqrt, pero solo registra como métodos las versiones en camelCase, faceForward e inverseSqrt. Es decir, x.inversesqrt() no existe aunque inversesqrt( x ) sí. Es exactamente el tipo de asimetría que cuesta diez minutos localizar.