Saltar al contenido

Varyings: el puente interpolado entre las dos etapas

Cómo viaja un valor del vertex shader al fragment shader, qué le hace el rasterizador por el camino, y las tres consecuencias que hay que tener siempre presentes.

El recorrido completo

Las cajas naranjas son tuyas. Las amarillas son hardware de función fija que no puedes modificar. El punto que hay que retener es que una varying se escribe tres veces por triángulo y se lee una vez por fragmento, y que entre esos dos momentos hay una operación matemática concreta.

Declarar una varying es escribir el mismo nombre y el mismo tipo en las dos etapas. Si no coinciden, el enlace falla.

GLSL
// Vertexvarying vec2 vUv;varying vec3 vNormal;varying vec3 vPosicionVista;void main() {  vUv = uv;  vNormal = normalMatrix * normal;          // ojo: sin normalizar aqui  vec4 posicionVista = modelViewMatrix * vec4( position, 1.0 );  vPosicionVista = posicionVista.xyz;  gl_Position = projectionMatrix * posicionVista;}
GLSL
// Fragmentvarying vec2 vUv;varying vec3 vNormal;varying vec3 vPosicionVista;void main() {  vec3 N = normalize( vNormal );            // imprescindible  vec3 V = normalize( -vPosicionVista );    // la camara esta en el origen del espacio de vista  float fresnel = pow( 1.0 - max( dot( N, V ), 0.0 ), 3.0 );  gl_FragColor = vec4( vec3( fresnel ), 1.0 );}

En el dialecto que Three.js compila, varying es en realidad una macro: se expande a out en el vertex shader y a in en el fragment shader. Puedes escribir in y out directamente si prefieres; el resultado es idéntico.

Qué hace la corrección de perspectiva

La interpolación ingenua tomaría las coordenadas baricéntricas del fragmento dentro del triángulo en pantalla y las usaría como pesos. Eso está mal en cuanto hay perspectiva, porque la proyección no conserva las razones de distancia: el punto medio de una arista en pantalla no corresponde al punto medio de la arista en el espacio.

El rasterizador lo corrige dividiendo cada varying por la componente w del clip space en los vértices, interpolando eso linealmente, e invirtiendo la división al final. El resultado es que la varying interpolada corresponde al valor real en el punto de la superficie, no en el punto de la pantalla.

Ese detalle es la razón por la que una textura sobre un suelo en perspectiva se ve correcta. Si desactivaras la corrección, obtendrías la deformación característica de los juegos de la primera PlayStation, donde las texturas se doblaban al girar la cámara.

Lo que no corrige es la no linealidad de la propia magnitud. La interpolación sigue siendo lineal en el valor de la varying, y ahí están las tres consecuencias.

Las tres consecuencias

Una dirección interpolada deja de ser unitaria. Tres normales unitarias en los vértices producen, en el centro del triángulo, la media de las tres, que tiene módulo menor que uno salvo que las tres coincidan. Cuanto más se abre el ángulo entre ellas, más se acorta. Por eso normalize( vNormal ) en el fragment shader no es un adorno defensivo: sin él, la intensidad difusa cae en el centro de cada triángulo y aparece un facetado suave que parece un problema de la malla.

Y por el mismo motivo, normalizar en el vertex shader no sirve: lo que hay que normalizar es el resultado de la interpolación, no las entradas.

Interpolar el resultado de una función no lineal es distinto de aplicar la función al valor interpolado. Ya lo vimos con la iluminación de Gouraud. Se aplica igual a un color en espacio no lineal, a un ángulo que cruza el origen, o a cualquier valor que hayas comprimido con una raíz o una potencia. La regla es: interpola la magnitud cruda y aplica la transformación después.

Cada varying cuesta. Ocupa un slot de interpolación y consume ancho de banda entre las dos etapas por cada fragmento. La especificación de WebGL 2 garantiza al menos sesenta componentes de varying, es decir quince vec4, y ése es un presupuesto que se agota antes de lo que parece: un shader con UV, normal, tangente, posición mundial, posición de vista y dos conjuntos de coordenadas de sombra ya va por doce. Cuando te falte sitio, empaqueta: dos vec2 caben en un vec4, y una normal se puede comprimir a dos componentes con codificación octaédrica.

El cualificador flat

flat desactiva la interpolación: todos los fragmentos del triángulo reciben el valor del vértice provocador, que es el último de los tres por convención en WebGL 2.

GLSL
// Vertexflat varying int vIndiceDeGrupo;// Fragmentflat varying int vIndiceDeGrupo;

Como varying se expande a out y a in, la construcción flat varying produce flat out y flat in, que es exactamente la sintaxis correcta.

Hay dos motivos para usarlo. El primero es que para los tipos enteros es obligatorio: int, uint, ivec y uvec no se pueden interpolar y el compilador lo rechaza sin flat. El segundo es que a veces quieres el valor de una cara, no una mezcla: un índice de material, un identificador de objeto, un color plano por triángulo.

El coste es menor que el de una varying interpolada, porque no hay que calcular nada por fragmento. Y hay un tercer uso menos conocido: flat sobre un float da sombreado facetado exacto sin necesidad de convertir la geometría a no indexada, siempre que el atributo de origen ya sea constante por cara.