Saltar al contenido

Swizzling: reordenar componentes sin coste

Los tres juegos de nombres, las reglas de lectura y de escritura, por qué el hardware no ejecuta ninguna instrucción al hacerlo, y los idiomas que aparecen en todo shader real.

Tres juegos de nombres para lo mismo

Cualquier vector se puede indexar por componente con tres conjuntos equivalentes:

Conjunto Componentes Uso previsto
x y z w posiciones y direcciones geometría
r g b a color y alfa color
s t p q coordenadas de textura muestreo

Los tres acceden exactamente a los mismos cuatro carriles del registro y el compilador los trata igual. Lo único prohibido es mezclarlos en un mismo swizzle: v.xg es un error, v.xy y v.rg no.

Elegir el conjunto adecuado es documentación gratis. Un vec4 que representa color se lee mejor como .rgb que como .xyz, y una coordenada de textura como .st. En un shader donde conviven posiciones y UV, la distinción evita confundir una cosa con la otra al leer.

Leer y escribir tienen reglas distintas

Al leer, un swizzle puede repetir componentes y ponerlas en cualquier orden, y produce un vector del tamaño del swizzle:

GLSL
vec4 v = vec4( 1.0, 2.0, 3.0, 4.0 );vec3 a = v.xyz;      // (1, 2, 3)vec3 b = v.zyx;      // (3, 2, 1)vec3 c = v.xxx;      // (1, 1, 1)   repetir esta permitidovec2 d = v.wz;       // (4, 3)float e = v.y;       // 2.0, un swizzle de una componente es un escalarvec4 f = v.xxyy;     // (1, 1, 2, 2)

Al escribir, cada componente de destino puede aparecer como mucho una vez, porque el hardware no sabe qué valor poner si le pides escribir dos cosas en el mismo carril:

GLSL
vec4 v;v.xy = vec2( 1.0, 2.0 );      // correctov.zw = vec2( 3.0, 4.0 );      // correctov.wx = vec2( 9.0, 8.0 );      // correcto: destino desordenado, sin repetirv.xx = vec2( 1.0, 2.0 );      // ERROR: x aparece dos veces

La restricción es sobre el destino, nunca sobre la fuente. Por eso v.xy = v.yy; es perfectamente válido.

Los vectores también admiten índice numérico, y ahí sí hay diferencia de coste: v[ i ] con i variable puede obligar al compilador a materializar el vector en memoria en vez de mantenerlo en registros. v.x es gratis, v[ 0 ] normalmente también porque es constante, y v[ i ] con i calculado puede no serlo.

Los idiomas que aparecen siempre

Coordenadas de pantalla normalizadas, el primer renglón de casi cualquier shader de pantalla completa:

GLSL
vec2 uv = gl_FragCoord.xy / uResolucion.xy;

Perspectiva a mano, la división por w cuando has calculado tú la posición en clip space:

GLSL
vec3 ndc = clipPos.xyz / clipPos.w;

Extraer la parte de rotación de una matriz de transformación, o su traslación:

GLSL
mat3 rotacion = mat3( uModelo );          // esquina 3x3vec3 traslacion = uModelo[ 3 ].xyz;       // cuarta columna

Componer y descomponer color:

GLSL
vec3 color = textura.rgb;float alfa = textura.a;gl_FragColor = vec4( color * alfa, alfa );        // premultiplicado

Escalar en dos ejes con un valor calculado en uno:

GLSL
vec2 aspecto = vec2( uResolucion.x / uResolucion.y, 1.0 );vec2 p = ( uv - 0.5 ) * aspecto;

Empaquetar dos parejas en un vec4 para ahorrar varyings o atributos, y desempaquetarlas al otro lado:

GLSL
// Vertexvarying vec4 vDatos;vDatos = vec4( uv, aSemilla, aRetardo );// Fragmentvec2 uvRecuperada = vDatos.xy;float semilla = vDatos.z;float retardo = vDatos.w;

Ese último es el uso que más rendimiento aporta: cuatro varyings de una componente cuestan cuatro slots de interpolación, y una de cuatro componentes cuesta uno.

Y un idioma menos evidente: usar un swizzle repetido para difundir un escalar sin llamar al constructor.

GLSL
vec3 gris = vec3( luminancia );      // constructorvec3 gris2 = luminancia * vec3( 1.0 );// Con un vector ya existente, .xxx difunde su primera componentevec3 desdeVector = valores.xxx;

El único coste real: la alineación en un bloque de uniforms

El swizzling es gratis, pero hay un sitio donde el tamaño de un vector sí cuesta, y como está relacionado conviene cerrarlo aquí. En un uniform buffer object —lo que Three.js expone como UniformsGroup— la disposición de memoria sigue las reglas std140, y esas reglas dicen que un vec3 se alinea a dieciséis bytes, igual que un vec4.

Es decir: un vec3 en un bloque de uniforms ocupa doce bytes útiles y cuatro de relleno. Una estructura con tres vec3 consecutivos ocupa cuarenta y ocho bytes, no treinta y seis. Y un array de vec3 es especialmente malo: cada elemento se alinea a dieciséis bytes, así que un array de cien vec3 gasta 1600 bytes para almacenar 1200.

GLSL
// Malo en std140: 48 bytes, 12 de rellenolayout( std140 ) uniform Malo {  vec3 posicion;  vec3 color;  vec3 direccion;};// Bueno: 48 bytes, 0 de relleno, y un escalar extra gratis en cada huecolayout( std140 ) uniform Bueno {  vec4 posicionYRadio;      // xyz posicion, w radio  vec4 colorEIntensidad;    // rgb color, a intensidad  vec4 direccionYAngulo;    // xyz direccion, w angulo};

Y una vez dentro del shader, recuperar cada campo es un swizzle, o sea gratis. Ése es el patrón que usan todos los motores para sus datos de luces.