Files
2026-04-28 22:12:27 +02:00

255 lines
9.7 KiB
Markdown

# 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:
```ts
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:
```ts
{
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 thumb
- `xy`: pad 2D que controla dos params contiguos
- `color`: picker RGB que controla tres params contiguos
- `select`: dropdown con opciones discretas (valor = índice)
- `source`: selector del nodo fuente para blends (escribe a `meta.sourceId`)
## Compilación del DAG a WGSL
`compileDagToWGSL(pipeline)` emite un shader con esta estructura:
```wgsl
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, pulse
- `blend`: 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) y `c` (color).
- Nuevo kind `warp` con snippets que transforman `uv`.
- 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`) vs `color` (`vec4<f32>`)
- Validación de tipos en compilación: un operador de color no acepta un field
- Nodo terminal `render_sdf` que 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
1. **Los IDs de nodo son UUIDs, no índices posicionales**. Las referencias
de `sourceId` sobreviven a reorderings. Los índices se re-derivan en
compilación.
2. **El patrón "armed drag"** para el drag handle: el nodo es `draggable=false`
por defecto y solo se arma a `true` cuando ocurre pointerdown sobre el
header con el handle. Esto evita que los sliders internos activen drag
accidentalmente.
3. **Uniform packing**: todos los parámetros de un nodo van en `u.params[idx]`
(un `vec4<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.
4. **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.
5. **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
## 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
1. `npm create vite@latest shader-dag -- --template react-ts`
2. Copiar `shader-dag-blends.jsx` como base monolítica, renombrar a `.tsx`
3. Arreglar los tipos TypeScript (muchas funciones del artifact no están tipadas)
4. Romper el monolito según la estructura de arriba
5. 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).