La familia de node materials
Las diecisiete clases que trae r184, qué relación tienen con los materiales clásicos, y el mecanismo por el que un slot vacío no cuesta nada.
Un node material no es un material distinto: es el mismo material de siempre con puntos de extensión. Acepta los mismos parámetros de constructor, produce la misma imagen si no tocas nada, y expone una colección de propiedades que, cuando les asignas un nodo, sustituyen exactamente una parte del cálculo dejando el resto intacto. Ese diseño es lo que resuelve, sin trucos, el problema del que arrancó el nivel treinta y cinco.
- Enumerar las clases de node material que existen en r184 y su equivalencia clásica.
- Construir un node material con los mismos parámetros que su versión clásica.
- Explicar el mecanismo por el que un slot sin asignar no altera el shader.
- Distinguir qué slots viven en la clase base y cuáles en cada subclase.
El catálogo
three/webgpu exporta diecisiete clases de material de nodos:
| Node material | Equivalente clásico |
|---|---|
NodeMaterial |
la base, sin equivalente directo |
MeshBasicNodeMaterial |
MeshBasicMaterial |
MeshLambertNodeMaterial |
MeshLambertMaterial |
MeshPhongNodeMaterial |
MeshPhongMaterial |
MeshStandardNodeMaterial |
MeshStandardMaterial |
MeshPhysicalNodeMaterial |
MeshPhysicalMaterial |
MeshToonNodeMaterial |
MeshToonMaterial |
MeshNormalNodeMaterial |
MeshNormalMaterial |
MeshMatcapNodeMaterial |
MeshMatcapMaterial |
MeshSSSNodeMaterial |
sin equivalente, dispersión subsuperficial |
PointsNodeMaterial |
PointsMaterial |
SpriteNodeMaterial |
SpriteMaterial |
LineBasicNodeMaterial |
LineBasicMaterial |
LineDashedNodeMaterial |
LineDashedMaterial |
Line2NodeMaterial |
el material de líneas gruesas de los addons |
ShadowNodeMaterial |
ShadowMaterial |
VolumeNodeMaterial |
sin equivalente, renderizado volumétrico |
La correspondencia con los nombres clásicos es literal: añade Node antes de Material. Y el constructor acepta el mismo objeto de parámetros:
import * as THREE from 'three/webgpu';
const material = new THREE.MeshPhysicalNodeMaterial( {
color: 0x0000ff,
metalness: 0.9,
roughness: 0.5,
clearcoat: 1.0,
clearcoatRoughness: 0.1,
normalMap: mapaNormal,
normalScale: new THREE.Vector2( 0.15, 0.15 )
} );
Migrar un proyecto sin tocar shaders es, por tanto, una sustitución de nombres. Si el material clásico funcionaba, el de nodos funciona igual.
Dos ausencias que conviene notar. MeshDepthNodeMaterial y MeshDistanceNodeMaterial no existen: en el sistema de nodos, la generación de shadow maps y de profundidad se resuelve por otra vía, con los slots de sombra de la clase base. Y InstancedPointsNodeMaterial tampoco está en el catálogo de r184.
Los slots de la clase base
NodeMaterial declara veintitrés propiedades que terminan en Node, todas inicializadas a null. Las que se usan a diario son estas:
| Slot | Qué sustituye |
|---|---|
colorNode |
el color difuso base |
positionNode |
la posición del vértice |
normalNode |
la normal de la superficie |
opacityNode |
la opacidad |
alphaTestNode |
el umbral del alpha test |
envNode |
la contribución del entorno |
aoNode |
la oclusión ambiental |
depthNode |
la profundidad escrita |
outputNode |
el color final, tras iluminación y salida |
fragmentNode |
el fragment shader entero |
vertexNode |
el vertex shader entero |
Y las de menor uso: lightsNode, backdropNode, backdropAlphaNode, maskNode, maskShadowNode, geometryNode, mrtNode, contextNode, castShadowNode, receivedShadowNode, castShadowPositionNode y receivedShadowPositionNode.
Cuidado con los dos últimos, porque el nombre corto que uno espera —shadowPositionNode— no existe. Son dos slots distintos, uno para cuando el objeto proyecta sombra y otro para cuando la recibe.
Los slots de cada subclase
Cada material añade lo suyo. MeshStandardNodeMaterial declara tres:
emissiveNode, metalnessNode, roughnessNode
MeshPhysicalNodeMaterial hereda esos tres y añade diecisiete más, uno por cada capa del modelo físico:
clearcoatNode, clearcoatRoughnessNode, clearcoatNormalNode,
sheenNode, sheenRoughnessNode,
iridescenceNode, iridescenceIORNode, iridescenceThicknessNode,
specularIntensityNode, specularColorNode, iorNode,
transmissionNode, thicknessNode,
attenuationDistanceNode, attenuationColorNode,
dispersionNode, anisotropyNode
Y MeshBasicNodeMaterial no declara ninguno propio: se conforma con los de la base, que es coherente con que su modelo de iluminación sea el más simple.
El método setupLighting() de la clase base lee emissiveNode sea cual sea el material, así que asignarlo funciona en cualquiera. Pero solo MeshStandardNodeMaterial y sus descendientes lo declaran en el constructor, y Material.setValues avisa por consola cuando le pasas una clave que la instancia no tiene definida. La consecuencia práctica: asígnalo como propiedad después de construir, no dentro del objeto del constructor, si el material no es Standard o Physical.
Por qué un slot vacío no cuesta nada
El mecanismo es una línea repetida por todo NodeMaterial.js, siempre con la misma forma:
let colorNode = this.colorNode ? vec4( this.colorNode ) : materialColor;
const opacityNode = this.opacityNode ? float( this.opacityNode ) : materialOpacity;
const metalnessNode = this.metalnessNode ? float( this.metalnessNode ) : materialMetalness;
let roughnessNode = this.roughnessNode ? float( this.roughnessNode ) : materialRoughness;
Cada slot es un ternario evaluado en tiempo de construcción del grafo, en JavaScript. Si el slot es null, se usa el accesor por defecto, que lee la propiedad correspondiente del material. Si tiene un nodo, se usa ese.
No hay ninguna rama en el shader generado. No hay coste en tiempo de ejecución. El material sin slots asignados produce exactamente el mismo grafo que produciría si el sistema de slots no existiera. Y un material con un slot asignado produce un grafo donde ese subárbol está sustituido, y nada más.
Compara esto con lo que hacía falta en el sistema clásico para conseguir lo mismo: encontrar el chunk correcto, sustituir una cadena, declarar la clave de caché, y esperar que la próxima versión no renombre nada.
Vale la pena poner nombre a lo que ha cambiado entre el nivel treinta y cinco y este, porque es un principio de diseño de librerías que trasciende los gráficos. onBeforeCompile es un mecanismo de extensión intrusiva: no hay contrato, hay acceso al interior. La librería te entrega su código fuente y tú lo modificas, lo cual significa que cualquier detalle interno —el nombre de un chunk, el nombre de una variable, el orden de dos líneas— se convierte de facto en API pública, sin que sus autores lo hayan decidido ni puedan saberlo. El resultado es que la librería no puede refactorizar nada sin romper a alguien, y que tú no puedes actualizar sin revisar. Los slots son extensión declarada: los autores han enumerado explícitamente los puntos donde admiten sustitución, les han dado nombre, tipo y semántica, y se han comprometido a mantenerlos. Todo lo que hay entre un slot y otro sigue siendo suyo y pueden reescribirlo entero mañana sin que tu código se entere. Fíjate en la asimetría de poder que eso corrige: con el modelo intrusivo, cada usuario que inyecta código congela un trozo de la implementación; con el declarado, la superficie congelada es exactamente la que los autores eligieron y ni un carácter más. Ahora bien, la extensión declarada tiene un coste, y es honesto reconocerlo: solo puedes hacer lo que alguien previó. Si necesitas sustituir algo para lo que no hay slot, el sistema de nodos no tiene un equivalente de replace(). Ese es el compromiso real de este diseño, y la razón de que la lista de slots de MeshPhysicalNodeMaterial sea tan larga: cada uno de esos diecisiete nombres es alguien que necesitó tocar una capa concreta y consiguió que le abrieran la puerta por la vía correcta, en lugar de forzarla.