Saltar al contenido

Un detalle propio sin renunciar al PBR

Por qué reescribir un ShaderMaterial para cambiar un detalle sale carísimo, qué hay realmente dentro de un MeshStandardMaterial, y las tres salidas posibles.

Lo que hay dentro de un material integrado

MeshStandardMaterial no tiene un shader propio. Su shaderID es physical, igual que MeshPhysicalMaterial, y ambos resuelven al mismo par de fuentes GLSL en ShaderLib.physical. Esas fuentes no son un bloque de código: son una lista ordenada de directivas #include que el motor expande justo antes de compilar.

El main() del fragment shader de meshphysical es literalmente esto, en este orden:

GLSL
void main() {	vec4 diffuseColor = vec4( diffuse, opacity );	#include <clipping_planes_fragment>	ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );	vec3 totalEmissiveRadiance = emissive;	#include <logdepthbuf_fragment>	#include <map_fragment>	#include <color_fragment>	#include <alphamap_fragment>	#include <alphatest_fragment>	#include <alphahash_fragment>	#include <roughnessmap_fragment>	#include <metalnessmap_fragment>	#include <normal_fragment_begin>	#include <normal_fragment_maps>	#include <clearcoat_normal_fragment_begin>	#include <clearcoat_normal_fragment_maps>	#include <emissivemap_fragment>	// accumulation	#include <lights_physical_fragment>	#include <lights_fragment_begin>	#include <lights_fragment_maps>	#include <lights_fragment_end>	// modulation	#include <aomap_fragment>	vec3 totalDiffuse = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse;	vec3 totalSpecular = reflectedLight.directSpecular + reflectedLight.indirectSpecular;	#include <transmission_fragment>	vec3 outgoingLight = totalDiffuse + totalSpecular + totalEmissiveRadiance;	#include <opaque_fragment>	#include <tonemapping_fragment>	#include <colorspace_fragment>	#include <fog_fragment>	#include <premultiplied_alpha_fragment>	#include <dithering_fragment>}

Lee la lista despacio, porque es el mapa de todo lo que estarías tirando a la basura. Antes del bloque de acumulación hay quince chunks que solo se ocupan de construir los parámetros de la superficie: leer el mapa difuso, aplicar el color de vértice, resolver el alpha test, muestrear rugosidad y metalidad, construir la normal desde el normal map y desde el bump map, y hacer lo mismo otra vez para el clearcoat. El bloque de acumulación resuelve la ecuación de renderizado para cada luz de la escena, más la contribución indirecta del entorno. Y los cinco últimos chunks son la salida: tone mapping, conversión al espacio de color de salida, niebla, alpha premultiplicado y dithering.

Añade a eso el vertex shader, con su cadena de beginnormal_vertex, skinbase_vertex, skinnormal_vertex, defaultnormal_vertex, begin_vertex, morphtarget_vertex, skinning_vertex, displacementmap_vertex y project_vertex, y tienes la respuesta a por qué escribir un ShaderMaterial “equivalente” es una tarea de semanas que además hay que rehacer cada vez que la librería cambia.

Las tres salidas

Ante la necesidad de un detalle propio hay exactamente tres caminos en r184, y cada uno paga un precio distinto.

Camino Qué te da Qué te cuesta
ShaderMaterial control total, código legible, cero sorpresas reimplementar PBR, sombras, IBL, tone mapping, color management
onBeforeCompile PBR completo con una inyección quirúrgica sustitución de cadenas frágil, dependiente de versión, difícil de depurar
Node materials y TSL PBR completo, composición declarativa, dos backends API distinta, three/webgpu en lugar de three, ecosistema más joven

El primero es correcto cuando el material no quiere ser PBR. Un efecto de holograma, una malla de puntos, un shader de agua estilizado, un fondo procedural: si no vas a necesitar sombras recibidas ni reflejos del entorno, el ShaderMaterial es más limpio, más rápido de compilar y mucho más fácil de mantener. Elegirlo por defecto es un error; descartarlo por defecto también.

El segundo es de lo que trata este nivel. Es la técnica que ha sostenido la mitad de los efectos bonitos de la web durante una década, y sigue siendo la única disponible si tu proyecto usa WebGLRenderer clásico con materiales integrados.

El tercero es hacia donde va la librería, y no lo digo yo. El propio Material.js de r184 lo dice en la documentación de onBeforeCompile:

Texto
This method can only be used when rendering with WebGLRenderer. Therecommended approach when customizing materials is to use WebGPURendererwith the new Node Material system and TSL.

Por qué esta técnica sigue existiendo

Si la recomendación oficial es otra, cabe preguntarse por qué dedicar cinco lecciones a onBeforeCompile. Tres razones.

La primera es que el código que existe ya está escrito así. Cualquier proyecto Three.js con más de dos años tiene inyecciones de este tipo, y entenderlas es requisito para tocarlas. La segunda es que el sistema de chunks no desaparece: es la estructura interna del renderer WebGL, y saber leerla te dice cosas sobre el coste real de cada opción del material que no están escritas en ninguna documentación. La tercera es que la migración a node materials no es un interruptor: implica cambiar el punto de entrada de three a three/webgpu y, si conservas el WebGLRenderer clásico, instalar un adaptador explícito. Mientras esa migración no ocurra, esta es la técnica que hay.