15 De ecuaciones a pixeles

LECCION 15 | ~25 min

De ecuaciones a pixeles

El pipeline completo desde las matematicas hasta la pantalla

El pipeline completo

Hemos recorrido un largo camino en este modulo. Empezamos con el problema fundamental (convertir ecuaciones en trayectorias), aprendimos Euler y sus limitaciones, derivamos RK4, y entendimos como elegir el paso temporal. Ahora es el momento de ver como se conecta todo en un pipeline funcional.

ChaosLab ejecuta un ciclo continuo que transforma ecuaciones matematicas en pixeles en pantalla. Este ciclo tiene 5 etapas, y cada frame (16.67 ms a 60 fps) las atraviesa todas. Veamos cada una:

La belleza de este pipeline es que es completamente general. Cambiando unicamente la funcion \(f(x)\) en la primera etapa, podemos visualizar cualquier sistema caotico: Lorenz, Rossler, Chen, Thomas, o cualquier otro. El resto del pipeline permanece identico.

Paso 1: Definir el sistema

Todo comienza con las ecuaciones. Para Lorenz, definimos una funcion JavaScript que calcula las derivadas:

function lorenz([x, y, z], params) {
    const { sigma, rho, beta } = params;
    return [
        sigma * (y - x),        // dx/dt
        x * (rho - z) - y,      // dy/dt
        x * y - beta * z        // dz/dt
    ];
}

Ademas de la funcion, necesitamos los parametros (\(\sigma = 10, \rho = 28, \beta = 8/3\)) y las condiciones iniciales (\(x_0 = 1, y_0 = 1, z_0 = 1\)). Con estos tres ingredientes, el sistema queda completamente definido.

Paso 2: Integrar con RK4

El integrador RK4 genera nuevos puntos de la trayectoria. En cada frame, calculamos un lote de puntos:

function rk4Step(f, state, params, dt) {
    const k1 = f(state, params);
    const k2 = f(add(state, scale(k1, dt/2)), params);
    const k3 = f(add(state, scale(k2, dt/2)), params);
    const k4 = f(add(state, scale(k3, dt)), params);
    return add(state, scale(
        add(k1, scale(k2, 2), scale(k3, 2), k4),
        dt / 6
    ));
}

// En el loop: 20 pasos por frame
for (let i = 0; i < 20; i++) {
    state = rk4Step(lorenz, state, params, 0.005);
    addToBuffer(state);
}

Con 20 pasos por frame y \(dt = 0.005\), cada frame avanza 0.1 unidades de tiempo simulado. A 60 fps, la simulacion avanza 6 unidades de tiempo por segundo. Para Lorenz, esto significa aproximadamente 4 orbitas completas por segundo -- una velocidad natural y agradable para la visualizacion.

Paso 3: Almacenar en buffer

Los puntos generados se almacenan en un buffer circular implementado con un Float32Array. La idea del buffer circular es elegante: en lugar de hacer push y shift (que requieren mover toda la memoria), simplemente sobrescribimos el punto mas antiguo.

const MAX_POINTS = 50000;
const positions = new Float32Array(MAX_POINTS * 3);
let writeIndex = 0;
let pointCount = 0;

function addToBuffer(state) {
    const i = (writeIndex % MAX_POINTS) * 3;
    positions[i]     = state[0];
    positions[i + 1] = state[1];
    positions[i + 2] = state[2];
    writeIndex++;
    pointCount = Math.min(pointCount + 1, MAX_POINTS);
}

Float32Array es clave por dos razones: (1) es mucho mas eficiente en memoria que un array de objetos JavaScript, y (2) puede pasarse directamente a la GPU a traves de WebGL/Three.js sin conversion. Para 50,000 puntos 3D, el buffer ocupa solo 600 KB (50000 * 3 * 4 bytes).

Paso 4: Renderizar con Three.js

Three.js toma el buffer de posiciones y lo convierte en geometria visible. El atractor se dibuja como una linea continua (THREE.Line) con un material que puede tener color, transparencia y efectos de brillo.

// Geometria
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position',
    new THREE.BufferAttribute(positions, 3));

// Material
const material = new THREE.LineBasicMaterial({
    color: 0xa855f7,
    transparent: true,
    opacity: 0.8
});

// Objeto
const line = new THREE.Line(geometry, material);
scene.add(line);

En cada frame, despues de calcular nuevos puntos, marcamos la geometria como actualizada (geometry.attributes.position.needsUpdate = true) y Three.js sube los datos a la GPU automaticamente. La GPU entonces rasteriza miles de segmentos de linea en microsegundos.

Paso 5: Mostrar en pantalla

El paso final es el render loop y los efectos de post-procesamiento. Three.js usa requestAnimationFrame para sincronizarse con la tasa de refresco del monitor. Opcionalmente, un EffectComposer aplica efectos como bloom (brillo difuso), tone mapping y anti-aliasing.

function animate() {
    requestAnimationFrame(animate);

    // 1. Integrar nuevos puntos
    for (let i = 0; i < 20; i++) {
        state = rk4Step(lorenz, state, params, dt);
        addToBuffer(state);
    }

    // 2. Actualizar geometria
    line.geometry.attributes.position.needsUpdate = true;
    line.geometry.setDrawRange(0, pointCount);

    // 3. Rotar camara
    camera.position.x = 60 * Math.cos(time * 0.1);
    camera.position.z = 60 * Math.sin(time * 0.1);
    camera.lookAt(0, 0, 25);

    // 4. Renderizar (con o sin post-procesado)
    composer.render();  // incluye bloom
}

El efecto bloom es especialmente importante para la estetica de ChaosLab. Toma los pixeles mas brillantes de la escena y les aplica un desenfoque gaussiano, creando el caracteristico "resplandor" de las lineas del atractor. Esto transforma una simple linea matematica en algo que parece un objeto luminoso flotando en el espacio.

ChaosLab ejecuta este pipeline 60 veces por segundo, generando 20 nuevos puntos por frame. Eso son 1,200 puntos por segundo, cada uno calculado con 4 evaluaciones de la funcion (RK4), para un total de 4,800 evaluaciones por segundo.

Etapa Entrada Salida Tecnologia
1. Definir sistema Ecuaciones + parametros Funcion f(state, params) JavaScript
2. Integrar (RK4) f(), estado, dt 20 puntos [x,y,z] RK4 (JS)
3. Buffer circular Puntos [x,y,z] Float32Array (50K * 3) TypedArray
4. Renderizar Float32Array BufferGeometry + Line Three.js / WebGL
5. Pantalla Escena 3D Pixeles con bloom EffectComposer

Ejercicios

  1. Si ChaosLab opera a 60 fps con 20 pasos/frame, cuantos puntos por segundo genera? A esa tasa, cuanto tarda en llenar un buffer de 50,000 puntos?
  2. Por que un buffer circular es mas eficiente que un array con push/shift? Piensa en la complejidad algoritmica de cada operacion.
  3. Que efecto visual produce el bloom sobre las lineas del atractor? Por que es importante para la percepcion de profundidad y la estetica?

LAB: El pipeline en accion

Diagrama animado del pipeline. Cada etapa se ilumina en secuencia mostrando el flujo de datos. Debajo, un mini atractor de Lorenz muestra el resultado final en tiempo real.

Pipeline ChaosLab Etapa 1: Definir sistema