255 lines
9.7 KiB
Markdown
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).
|