ShaderChunk, ShaderLib y el punto de inserción
Cómo está organizado el GLSL de Three.js por dentro, cómo encontrar el chunk exacto donde tu código tiene que entrar, y qué variables hay en alcance en cada punto.
Las dos estructuras
ShaderChunk es un objeto plano exportado desde three. Sus claves son de dos familias. La primera son los fragmentos individuales, en snake_case: common, begin_vertex, project_vertex, map_fragment, lights_fragment_begin, worldpos_vertex y compañía. La segunda son los shaders completos, con sufijo _vert o _frag: meshphysical_vert, meshphysical_frag, sprite_vert, background_frag.
ShaderLib es el catálogo de materiales. Sus claves son basic, lambert, phong, standard, toon, matcap, points, dashed, depth, normal, sprite, background, backgroundCube, cube, equirect, distance, shadow y physical. Cada entrada tiene exactamente la forma { uniforms, vertexShader, fragmentShader }.
Un detalle que sorprende: ShaderLib.standard existe pero ningún material lo selecciona. La tabla shaderIDs de WebGLPrograms mapea tanto MeshStandardMaterial como MeshPhysicalMaterial a physical. Las dos entradas comparten el mismo GLSL —ShaderChunk.meshphysical_vert y meshphysical_frag— y solo difieren en el conjunto de uniforms declarados. Si vas a inspeccionar el shader de un MeshStandardMaterial, mira ShaderLib.physical.
La forma más rápida de trabajar es imprimirlos:
import { ShaderChunk, ShaderLib } from 'three';// Todos los chunks disponibles, ordenados.console.log( Object.keys( ShaderChunk ).sort() );// El vertex shader completo de MeshStandardMaterial, sin expandir.console.log( ShaderLib.physical.vertexShader );// El contenido literal de un chunk concreto.console.log( ShaderChunk.lights_fragment_begin );Ese último console.log es la herramienta que de verdad importa. Antes de inyectar nada, lee el chunk que vas a usar como ancla y comprueba qué declara, qué modifica y con qué nombre exacto.
Los cuatro puntos canónicos
Casi toda inyección útil cae en uno de cuatro sitios. Vale la pena memorizarlos junto con la variable que se toca en cada uno.
Declaraciones. Uniforms, varyings y funciones auxiliares van antes del cuerpo de main(), ancladas al primer include de la sección de declaraciones. En ambos shaders ese include es common:
shader.fragmentShader = shader.fragmentShader.replace( '#include <common>', `#include <common> uniform vec3 uTinte; varying vec3 vPosLocal;`);El chunk common es también donde el motor define constantes que puedes reutilizar sin declararlas: PI, PI2, PI_HALF, RECIPROCAL_PI, EPSILON, la macro saturate( a ) y whiteComplement( a ). Redeclararlas es un error de compilación.
El vértice. Después de begin_vertex, que declara vec3 transformed = vec3( position ). Todo lo que hagas a transformed lo recogen morphtarget_vertex, skinning_vertex, displacementmap_vertex y finalmente project_vertex, cuyo cuerpo es:
vec4 mvPosition = vec4( transformed, 1.0 );#ifdef USE_BATCHING mvPosition = batchingMatrix * mvPosition;#endif#ifdef USE_INSTANCING mvPosition = instanceMatrix * mvPosition;#endifmvPosition = modelViewMatrix * mvPosition;gl_Position = projectionMatrix * mvPosition;Si necesitas la posición en espacio de mundo dentro del fragment shader, el ancla es worldpos_vertex, que va después de project_vertex y deja disponible worldPosition.
El color difuso. Después de map_fragment, que es el chunk que multiplica la textura difusa sobre diffuseColor:
#ifdef USE_MAP vec4 sampledDiffuseColor = texture2D( map, vMapUv ); #ifdef DECODE_VIDEO_TEXTURE sampledDiffuseColor = sRGBTransferEOTF( sampledDiffuseColor ); #endif diffuseColor *= sampledDiffuseColor;#endifNota que el chunk multiplica en lugar de asignar, y que respeta #ifdef USE_MAP. Si inyectas detrás, diffuseColor ya tiene el color base del material combinado con la textura si la hay. Ese es el sitio para un patrón procedural, un tinte por posición o una máscara. El valor está en espacio lineal, no en sRGB, y es un vec4 cuyo canal alfa importa.
La iluminación. Alrededor de lights_fragment_begin. Ese chunk empieza así:
vec3 geometryPosition = - vViewPosition;vec3 geometryNormal = normal;vec3 geometryViewDir = ( isOrthographic ) ? vec3( 0, 0, 1 ) : normalize( vViewPosition );vec3 geometryClearcoatNormal = vec3( 0.0 );y después recorre las luces acumulando en reflectedLight. Si inyectas antes, puedes falsear la normal geométrica o la dirección de vista y todo el cálculo posterior te seguirá. Si inyectas después de lights_fragment_end, tienes reflectedLight poblado con sus cuatro componentes: directDiffuse, indirectDiffuse, directSpecular, indirectSpecular.
Para tocar el color final, el ancla es la línea que declara outgoingLight, que no es un include sino código literal del shader:
shader.fragmentShader = shader.fragmentShader.replace( 'vec3 outgoingLight = totalDiffuse + totalSpecular + totalEmissiveRadiance;', `vec3 outgoingLight = totalDiffuse + totalSpecular + totalEmissiveRadiance; outgoingLight = mix( outgoingLight, uTinte, 0.25 );`);Fíjate en que esta inyección va antes de tonemapping_fragment y colorspace_fragment. Eso es lo correcto: estás modificando radiancia lineal, y la conversión a espacio de pantalla ocurre después, una sola vez, donde debe.
Registrar un chunk propio
ShaderChunk es un objeto normal, así que puedes añadirle claves. El expansor de includes las resolverá igual que las nativas:
import { ShaderChunk } from 'three';ShaderChunk.ruido_valor = /* glsl */` float hash21( vec2 p ) { p = fract( p * vec2( 233.34, 851.73 ) ); p += dot( p, p + 23.45 ); return fract( p.x * p.y ); } float ruidoValor( vec2 p ) { vec2 i = floor( p ); vec2 f = fract( p ); vec2 u = f * f * ( 3.0 - 2.0 * f ); return mix( mix( hash21( i + vec2( 0.0, 0.0 ) ), hash21( i + vec2( 1.0, 0.0 ) ), u.x ), mix( hash21( i + vec2( 0.0, 1.0 ) ), hash21( i + vec2( 1.0, 1.0 ) ), u.x ), u.y ); }`;material.onBeforeCompile = ( shader ) => { shader.fragmentShader = shader.fragmentShader.replace( '#include <common>', `#include <common> #include <ruido_valor>` ); shader.fragmentShader = shader.fragmentShader.replace( '#include <map_fragment>', `#include <map_fragment> float n = ruidoValor( vMapUv * 12.0 ); diffuseColor.rgb *= mix( 0.7, 1.0, n );` );};Esto tiene una ventaja real sobre pegar el GLSL en la cadena: la biblioteca de funciones vive en un solo sitio, se reutiliza entre materiales y no engorda el texto de onBeforeCompile, que es lo que por defecto forma la clave de caché del programa.
Tiene también un riesgo evidente. ShaderChunk es un objeto global compartido con la librería: si eliges un nombre que Three.js use en una versión futura, lo pisas silenciosamente y rompes todos los materiales. Prefija siempre.