9.7 KiB
Shader DAG Lab — Arquitectura y hoja de ruta
Este documento resume el diseño acumulado tras iterar en artifacts desde "composición funcional sobre listas" hasta "DAG de shaders WebGPU con fan-in". Sirve como contexto para retomar el proyecto en Claude Code sin perder las decisiones de diseño.
El problema que resuelve
Un entorno para componer fragment shaders WGSL visualmente: el usuario arrastra "nodos" (primitivas shader) desde una paleta a un pipeline, configura parámetros con sliders / XY pads / color pickers, y el sistema compila el DAG resultante a un único fragment shader ejecutado en WebGPU.
Las dos vistas — grafo y código — son proyecciones del mismo modelo interno. El usuario casual arrastra cajas; el usuario avanzado lee el WGSL generado.
Arquitectura en una frase
Pipeline state (árbol JSON)
↓ compileDagToWGSL()
WGSL source
↓ device.createShaderModule()
GPU pipeline
↓ render loop, escribe uniforms cada frame
Canvas
Separación crítica: la topología del DAG (qué nodos, en qué orden, con qué aristas) dispara recompilación del shader. Los valores de parámetros NO disparan recompilación — solo se escriben al uniform buffer cada frame. Esto hace que mover un slider sea instantáneo mientras que añadir/quitar nodos paga el coste de compilar un nuevo pipeline.
Modelo de datos
Un step del pipeline es:
type Step = {
id: string; // UUID estable (sobrevive reorders)
name: string; // clave en el catálogo NODES
params: { // valores de parámetros editables
[key: string]: number
};
meta?: { // metadatos que afectan compilación
sourceId?: string; // para blends: id del otro nodo fuente
};
};
El topologyKey que dispara recompilación se computa como:
pipeline.map(s => `${s.name}:${s.meta?.sourceId ?? ''}`).join('|')
Catálogo de nodos
Cada entrada en NODES declara:
{
kind: 'gen' | 'op' | 'blend' | 'warp' | 'sdf' | 'filter' | 'modulator',
label: string,
desc: string,
params: [{ k: string, d: number }], // hasta 4 slots del vec4<f32>
controls: Control[], // descriptores de UI
body: (idx: number) => string, // emite el cuerpo WGSL
}
Tipos de control UI actualmente soportados
slider: rango numérico con thumbxy: pad 2D que controla dos params contiguoscolor: picker RGB que controla tres params contiguosselect: dropdown con opciones discretas (valor = índice)source: selector del nodo fuente para blends (escribe ameta.sourceId)
Compilación del DAG a WGSL
compileDagToWGSL(pipeline) emite un shader con esta estructura:
struct Uniforms {
time: f32,
_pad: f32,
resolution: vec2<f32>,
params: array<vec4<f32>, 16>,
};
@group(0) @binding(0) var<uniform> u: Uniforms;
@vertex fn vs(...) { /* fullscreen triangle */ }
// Una función por nodo, nombrada node_<idx>
fn node_0(c: vec4<f32>, uv: vec2<f32>) -> vec4<f32> { ... }
fn node_1(a: vec4<f32>, b: vec4<f32>, uv: vec2<f32>) -> vec4<f32> { ... } // blend
@fragment fn fs(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
let uv = pos.xy / u.resolution;
let out_0 = node_0(vec4<f32>(0.0), uv);
let out_1 = node_1(out_0, out_0, uv); // blend con source=out_0
return out_1;
}
Los outputs intermedios out_<i> se preservan como variables locales para
que los blends puedan referenciar nodos anteriores arbitrarios. Esto es lo
que hace que el DAG sea más que un pipeline lineal.
Estado actual de tipos de nodo
Implementados (lab 004):
gen: solid, gradient, plasma, checker, circle, stripes, noise (hash)op: invert, gamma, contrast, saturate, hueShift, tint, posterize, vignette, ripple, pulseblend: mix, multiply, screen, add, difference, darken, lighten, mask
Pendientes de implementar (ver hoja de ruta abajo):
warp: distorsiones de UV (twirl, polar, kaleidoscope, pixelate, chromatic)sdf: campos de distancia con compositing (smooth_union, subtract, intersect)modulator: LFOs que producen escalares animados para alimentar parámetros- Ruidos procedurales reales: perlin, simplex, worley, fbm
- Filtros de luminancia: threshold, levels, duotone, channel_swap
- Inputs externos: mouse como uniform adicional
Hoja de ruta post-artifact
Lab 005 — Warps + Ruidos + Filtros de luma + Mouse
Cambios que requiere:
- Refactor de compilación: generadores pasan a ser
fn sample_gen_<i>(uv) -> vec4<f32>para que los warps puedan modificar uv antes del muestreo. - La cadena main mantiene dos estados:
uv(mutable por warps) yc(color). - Nuevo kind
warpcon snippets que transformanuv. - Nuevo uniform
mouse: vec2<f32>(coords 0..1) actualizado desde pointer events. - Perlin/FBM como snippets WGSL copiados de implementaciones conocidas (hash-based gradient noise).
Lab 006 — Multi-pass (convoluciones + feedback)
Cambio arquitectónico mayor: cada nodo puede escribir a una textura offscreen, el siguiente samplea esa textura. Requiere:
- Render targets intermedios (pool de texturas)
- Múltiples bind groups
- Double buffer para feedback temporal (frame N+1 lee frame N)
- Detección de qué nodos necesitan aislar su pass y cuáles pueden fusionarse
Desbloquea: blur gaussiano, sobel, edge detection, bloom, reaction-diffusion, trails, motion blur.
Lab 007 — SDFs tipados
Introduce heterogeneidad de tipos en las aristas del DAG:
- Aristas de tipo
field(f32) vscolor(vec4<f32>) - Validación de tipos en compilación: un operador de color no acepta un field
- Nodo terminal
render_sdfque convierte field → color con shading opciones (planar, gradient, stroke, inflate/outline) - Operadores SDF:
smooth_union,subtract,intersect,round,onion
Este es el salto conceptual a "DAG tipado" que formaliza lo que el lab 001 insinuaba (el fold cambiaba de tipo la arista).
Lab 008 — Bidireccional código ↔ grafo
Hasta ahora solo va grafo → código. El inverso requiere:
- Parser acotado del WGSL que nosotros mismos emitimos (no WGSL general)
- Marcadores en comentarios
// @meta node=<name> id=<id>para robustez - Detección de diff estructural para mantener posiciones / parámetros al editar
- Editor de código integrado (CodeMirror) sincronizado con el pipeline
Lab 009 — Nodos custom definidos por usuario
Modal donde el usuario escribe body WGSL + declara params, y se registra en la paleta como si viniera del catálogo. Cierra la asimetría código→grafo sin necesitar parser completo. Persiste en localStorage o export/import JSON.
Decisiones de diseño que vale la pena recordar
-
Los IDs de nodo son UUIDs, no índices posicionales. Las referencias de
sourceIdsobreviven a reorderings. Los índices se re-derivan en compilación. -
El patrón "armed drag" para el drag handle: el nodo es
draggable=falsepor defecto y solo se arma atruecuando ocurre pointerdown sobre el header con el handle. Esto evita que los sliders internos activen drag accidentalmente. -
Uniform packing: todos los parámetros de un nodo van en
u.params[idx](unvec4<f32>). Si un nodo necesita más de 4 floats, habría que reasignar slots o usar dos slots. No hay nodos hasta ahora que lo pidan. -
MAX_NODES = 16 es arbitrario, limitado solo por el tamaño del array de params en el uniform buffer. Subir es trivial: cambia la constante.
-
Las arbitrary values de Tailwind no funcionan en algunos entornos sin JIT. Grid templates, min-h con calc, etc. se escriben con
style={{...}}inline. En el proyecto de Claude Code esto no debería ser problema si usas Tailwind 3+ con su compilador.
Stack sugerido para Claude Code
- Vite + React + TypeScript: setup estándar, HMR inmediato
- Tailwind 3+ con JIT: los arbitrary values funcionarán esta vez
- Zustand o Jotai para el pipeline state (se va a hacer más complejo)
- Biome o ESLint + Prettier para formato consistente
- Opcional pero recomendado en cuanto crezca:
- Vitest para tests unitarios del compilador (
compileDagToWGSL) - React Testing Library si tests de UI
- reactflow si en lab 008 quieres visualizar el DAG como grafo editable
- Vitest para tests unitarios del compilador (
Organización de archivos sugerida
src/
nodes/
index.ts # export del catálogo completo
generators.ts # gen kind
operators.ts # op kind
blends.ts # blend kind
warps.ts # (lab 005)
sdfs.ts # (lab 007)
types.ts # tipos compartidos NodeDef, Control, etc.
compiler/
compileDagToWGSL.ts # la función principal
uniforms.ts # packing y escritura del buffer
validate.ts # validación de tipos (lab 007+)
webgpu/
useWebGPU.ts # el hook
renderer.ts # setup del device, context, pipeline
ui/
PipelineNode.tsx
controls/
Slider.tsx
XYPad.tsx
ColorPicker.tsx
Select.tsx
SourceSelector.tsx
Palette.tsx
Canvas.tsx
WGSLView.tsx
store/
pipeline.ts # Zustand store
App.tsx
main.tsx
Primeros pasos en Claude Code
npm create vite@latest shader-dag -- --template react-ts- Copiar
shader-dag-blends.jsxcomo base monolítica, renombrar a.tsx - Arreglar los tipos TypeScript (muchas funciones del artifact no están tipadas)
- Romper el monolito según la estructura de arriba
- Implementar lab 005: refactor de compilación para habilitar warps
Nota: el artifact de base (shader-dag-blends.jsx) funciona pero tiene el
tipado implícito de JSX. Convertir a TS te va a revelar varios tipos que
merece la pena modelar explícitamente (especialmente Control, que ahora es
un union discriminado informal).