ShaderMaterial frente a RawShaderMaterial
Exactamente qué prefijo inyecta Three.js r184 en cada etapa, por qué tu ShaderMaterial en realidad se compila como GLSL ES 3.00, y cuándo tiene sentido renunciar a todo eso.
Entre la cadena de GLSL que escribes y la que el driver compila hay unas ochenta líneas que Three.js pone por ti. Saber cuáles son deja de ser una curiosidad en cuanto un error de compilación te señala una línea que tú no escribiste, y se vuelve decisivo cuando descubres que el dialecto real es dos versiones más moderno del que casi todos los tutoriales asumen.
- Enumerar los uniforms y atributos que Three.js declara en cada etapa, y los que no.
- Explicar por qué un
ShaderMaterialcompila como GLSL ES 3.00 y qué desbloquea eso. - Escribir el mismo shader en las dos clases y comparar lo que hay que declarar.
- Decidir cuándo
RawShaderMateriales la elección correcta.
El prefijo exacto de r184
Cuando el material no es un RawShaderMaterial, WebGLProgram antepone a tu vertex shader un bloque con la precisión, las macros de configuración y estas declaraciones fijas:
uniform mat4 modelMatrix;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat3 normalMatrix;
uniform vec3 cameraPosition;
uniform bool isOrthographic;
attribute vec3 position;
attribute vec3 normal;
attribute vec2 uv;
Esos tres atributos están siempre. Los demás aparecen condicionados a macros que el renderer activa según la geometría y el material: uv1, uv2, uv3, tangent, color, skinIndex y skinWeight, más instanceMatrix e instanceColor cuando el objeto es un InstancedMesh.
El prefijo del fragment shader es mucho más corto, y ahí está la primera sorpresa útil:
uniform mat4 viewMatrix;
uniform vec3 cameraPosition;
uniform bool isOrthographic;
Tres uniforms y nada más. En el fragment shader no tienes modelViewMatrix, ni projectionMatrix, ni normalMatrix, ni modelMatrix. Si los necesitas allí, o pasas el resultado por una varying o los declaras tú como uniforms propios. Es un error de compilación frecuentísimo y el mensaje del driver no ayuda nada.
Además de eso, el prefijo del fragment shader incluye las funciones de espacio de color y de luminancia que Three.js usa para la salida, y las de tone mapping cuando hay alguno activo. Por eso linearToOutputTexel y luminance están disponibles sin declararlas.
Y una macro que sirve para depurar: SHADER_NAME toma el valor de material.name, lo cual hace que un volcado del shader se pueda identificar de un vistazo.
Tu ShaderMaterial es GLSL ES 3.00
Éste es el punto que contradice casi toda la documentación de terceros que circula. En WebGLProgram, para cualquier material que no sea RawShaderMaterial, la cadena de versión se fija sin condiciones:
versionString = '#version 300 es\n';
Y para que el código escrito en el dialecto antiguo siga compilando, se añaden macros de compatibilidad. En el vertex shader:
#define attribute in
#define varying out
#define texture2D texture
Y en el fragment shader, además de #define varying in, se declara la salida y se falsifica gl_FragColor:
layout(location = 0) out highp vec4 pc_fragColor;
#define gl_FragColor pc_fragColor
#define texture2D texture
#define textureCube texture
Las dos líneas de la salida se omiten si tú has puesto glslVersion: THREE.GLSL3, porque entonces se asume que declaras tu propia salida.
Las consecuencias prácticas son grandes, y todas positivas. En un ShaderMaterial corriente, sin tocar glslVersion, tienes disponible todo GLSL ES 3.00: el operador % sobre enteros, los operadores de bits, el tipo uint y uvec, texelFetch y textureSize para leer téxeles por índice sin filtrado, textureLod y textureGrad, bucles con límites no constantes, switch, la variante de mix con selector booleano, los cualificadores de interpolación flat y centroid, y gl_VertexID e gl_InstanceID.
// Todo esto compila en un ShaderMaterial normal de r184
uniform sampler2D uDatos;
uint hash( uint x ) {
x ^= x >> 16u;
x *= 0x7feb352du;
x ^= x >> 15u;
return x;
}
void main() {
ivec2 tamano = textureSize( uDatos, 0 );
vec4 crudo = texelFetch( uDatos, ivec2( gl_FragCoord.xy ) % tamano, 0 );
float r = float( hash( uint( gl_FragCoord.x ) ) ) / 4294967296.0;
gl_FragColor = vec4( crudo.rgb * r, 1.0 );
}
Y una trampa que se deriva de lo anterior: si declaras out vec4 algo; en el fragment shader sin poner glslVersion: THREE.GLSL3, chocas con pc_fragColor, que ya ocupa la localización 0. El enlace falla con un mensaje confuso sobre salidas duplicadas. O usas gl_FragColor, o cambias la versión y te haces cargo de la salida entera.
RawShaderMaterial: nada de nada
Con RawShaderMaterial el prefijo se reduce a SHADER_TYPE, SHADER_NAME y tus propios defines. No hay versión —así que por defecto es GLSL ES 1.00—, no hay precisión, y no hay una sola declaración de uniform ni de atributo. El mismo shader mínimo, escrito para las dos clases:
// ShaderMaterial: esto es todo
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
// RawShaderMaterial: hay que declararlo absolutamente todo
precision highp float;
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
attribute vec3 position;
attribute vec2 uv;
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
Fíjate en que los nombres tienen que ser exactos: el renderer busca uniforms llamados projectionMatrix y modelViewMatrix por nombre y les asigna los valores que corresponden. Con RawShaderMaterial no dejas de recibirlos; solo dejas de recibir su declaración.
¿Cuándo compensa? En tres casos y solo tres. Cuando quieres control absoluto sobre la versión y el preámbulo, típicamente para escribir GLSL ES 3.00 explícito con salidas múltiples. Cuando tu shader no necesita casi ninguno de los uniforms inyectados y quieres un programa mínimo —un paso de post-procesado a pantalla completa, por ejemplo, no necesita normalMatrix ni cameraPosition. Y cuando estás portando un shader escrito para otro entorno y prefieres no pelearte con lo que Three.js ya haya declarado.
Fuera de eso, ShaderMaterial es la elección por defecto, entre otras cosas porque las directivas #include de la biblioteca de chunks de Three.js se resuelven en las dos clases, pero los chunks asumen las declaraciones del prefijo.
Una limitación que no se puede rodear
ShaderMaterial y RawShaderMaterial funcionan únicamente con WebGLRenderer. No hay conversión automática a WGSL ni camino de compatibilidad: si un día mueves el proyecto a WebGPURenderer, tus shaders de GLSL no viajan. El equivalente en ese mundo son los materiales de nodos y TSL, que compilan el mismo grafo a los dos destinos.
Eso no es un argumento para no aprender GLSL —el modelo mental es idéntico y TSL solo cambia la sintaxis— pero sí para no construir un sistema de materiales entero sobre onBeforeCompile y cadenas de GLSL si sabes que la migración está en el plan.
La cantidad de tutoriales, respuestas de foro y cursos que afirman que un ShaderMaterial de Three.js es GLSL ES 1.00 es abrumadora, y llevan razón para cualquier versión anterior al abandono de WebGL 1. Desde entonces la conversión a #version 300 es es incondicional, y lo único que sostiene la ilusión del dialecto antiguo son cuatro #define. Esto tiene una consecuencia que casi nadie aprovecha y que cambia de verdad lo que puedes escribir: los hashes enteros funcionan. Toda la generación de ruido en GLSL clásico se apoya en fract( sin( dot( ... ) ) * 43758.5453 ), un truco que depende de la imprecisión de sin y que da resultados distintos en cada GPU. Con enteros sin signo y operadores de bits disponibles puedes usar un hash de verdad, tipo PCG, que es determinista bit a bit en cualquier hardware, más rápido que una transcendental y con propiedades estadísticas comprobadas. Lo mismo con texelFetch: leer un téxel por índice sin filtrado ni normalización de coordenadas elimina de un plumazo toda la clase de bugs de medio téxel que aparecen al usar texturas como tablas de datos. La forma de comprobarlo en tu propio proyecto en treinta segundos es escribir uint x = 1u; x = x >> 1u; en un fragment shader: si compila, estás en 3.00, y si estás en 3.00 lo estás desde hace años sin haberlo usado.