Saltar al contenido

El primer shader en TSL

Una escena completa que funciona: imports correctos, renderer inicializado, un material de nodos con color procedural y vértices animados.

La escena completa

JS
import * as THREE from 'three/webgpu';import {  color, mix, sin, time, uniform, positionLocal, normalLocal} from 'three/tsl';import { OrbitControls } from 'three/addons/controls/OrbitControls.js';const renderer = new THREE.WebGPURenderer( { antialias: true } );renderer.setPixelRatio( Math.min( devicePixelRatio, 2 ) );renderer.setSize( innerWidth, innerHeight );renderer.toneMapping = THREE.ACESFilmicToneMapping;document.body.appendChild( renderer.domElement );const escena = new THREE.Scene();escena.background = new THREE.Color( 0x0b0d14 );const camara = new THREE.PerspectiveCamera( 45, innerWidth / innerHeight, 0.1, 100 );camara.position.set( 0, 1.5, 5 );const controles = new OrbitControls( camara, renderer.domElement );controles.enableDamping = true;escena.add( new THREE.HemisphereLight( 0x99bbff, 0x221100, 0.6 ) );const sol = new THREE.DirectionalLight( 0xffffff, 2.5 );sol.position.set( 3, 5, 2 );escena.add( sol );// --- El material ---const uAmplitud = uniform( 0.12 );const material = new THREE.MeshStandardNodeMaterial( {  roughness: 0.35,  metalness: 0.1} );// Color: dos tonos mezclados por una onda que recorre la geometria.const onda = sin( positionLocal.x.mul( 4 ).add( time.mul( 1.5 ) ) ).mul( 0.5 ).add( 0.5 );material.colorNode = mix( color( 0x1f4fd8 ), color( 0xff7a3d ), onda );// Vertices: desplazamiento a lo largo de la normal, con la misma onda.material.positionNode = positionLocal.add( normalLocal.mul( onda ).mul( uAmplitud ) );const malla = new THREE.Mesh(  new THREE.SphereGeometry( 1.2, 128, 128 ),  material);escena.add( malla );// --- Bucle ---function redimensionar() {  renderer.setSize( innerWidth, innerHeight );  camara.aspect = innerWidth / innerHeight;  camara.updateProjectionMatrix();}addEventListener( 'resize', redimensionar );renderer.setAnimationLoop( () => {  controles.update();  renderer.render( escena, camara );} );

Funciona tal cual, y merece la pena señalar lo que no hay: ni una línea de GLSL, ni un onBeforeCompile, ni una clave de caché, ni un ancla de sustitución. Y sin embargo el material sigue siendo un MeshStandardNodeMaterial completo: recibe las dos luces, respeta la rugosidad, respeta el metalness, participaría en las sombras y en la iluminación de entorno si las hubiera.

Fíjate también en que no hay await renderer.init(). Es correcto porque setAnimationLoop inicializa el renderer por dentro antes de ejecutar el primer callback. Si en lugar de un bucle continuo quisieras renderizar bajo demanda, sí haría falta esperar la inicialización, y eso se ve en el nivel de WebGPURenderer.

Leyendo el material línea a línea

JS
const onda = sin( positionLocal.x.mul( 4 ).add( time.mul( 1.5 ) ) ).mul( 0.5 ).add( 0.5 );

positionLocal es la posición del vértice en espacio de objeto, la misma que position en GLSL. .x extrae la componente. .mul( 4 ) fija la frecuencia espacial. time es un uniforme que Three.js actualiza solo en cada frame: no hay que declararlo ni alimentarlo. .mul( 1.5 ) es la velocidad. sin() da un valor entre menos uno y uno, y el .mul( 0.5 ).add( 0.5 ) final lo lleva al rango de cero a uno, que es el que necesita una mezcla.

JS
material.colorNode = mix( color( 0x1f4fd8 ), color( 0xff7a3d ), onda );

colorNode sustituye el color base del material. mix( a, b, t ) interpola desde a hacia b según t, con el mismo orden de argumentos que en GLSL. color() construye un nodo de color a partir de un hexadecimal, un THREE.Color o tres componentes.

JS
material.positionNode = positionLocal.add( normalLocal.mul( onda ).mul( uAmplitud ) );

positionNode sustituye la posición del vértice. Desplazar a lo largo de normalLocal infla y desinfla la esfera siguiendo la misma onda. Y uAmplitud, que es un uniform, controla la magnitud desde JavaScript.

Cambiar un uniform en caliente

Un uniform() de TSL guarda su valor en la propiedad .value, igual que un uniform clásico:

JS
uAmplitud.value = 0.3;

Y como es un objeto normal, se puede enganchar directamente a un panel de control:

JS
import GUI from 'three/addons/libs/lil-gui.module.min.js';const gui = new GUI();gui.add( uAmplitud, 'value', 0, 0.5, 0.001 ).name( 'Amplitud' );

Los uniformes de color se construyen desde un THREE.Color y se mutan con sus métodos:

JS
const uTinte = uniform( new THREE.Color( 0xff7a3d ) );material.colorNode = mix( color( 0x1f4fd8 ), uTinte, onda );// Mas tarde:uTinte.value.set( 0x33ff88 );

La diferencia clave con un valor literal es que cambiar un uniform no recompila nada. color( 0xff7a3d ) es una constante que se hornea en el código generado; uniform( new THREE.Color( 0xff7a3d ) ) es un dato que viaja a la GPU en cada frame. La regla es la misma que con GLSL: lo que cambia va en un uniform, lo que no cambia va como constante y le da más margen al compilador.