Files
shaders_lab/extracted/ARCHITECTURE.md
T
2026-04-28 22:12:27 +02:00

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 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:

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).