WebGL координация

Работа с Popmotion в связке с WebGL требует точного согласования двух независимых систем времени и состояния: анимационного движка JavaScript и GPU-конвейера. WebGL оперирует буферами, шейдерами и состоянием контекста, тогда как Popmotion управляет потоками значений, интерполяциями и физическими моделями движения.

Ключевая задача — обеспечить непрерывное преобразование анимационных значений в параметры GPU без рассинхронизации кадров, скачков и потери производительности.


Базовая модель синхронизации Popmotion и WebGL

Popmotion строится вокруг концепции потоков значений (value streams), которые обновляются на каждом кадре requestAnimationFrame. WebGL, в свою очередь, требует явного обновления буферов и uniform-переменных перед каждым рендером.

Основной принцип координации:

  • Popmotion генерирует состояние (позиции, углы, масштаб, цвет)
  • WebGL потребляет это состояние при отрисовке кадра
  • единый RAF-цикл служит точкой синхронизации

Центральный цикл

import { animate } from 'popmotion';

const state = {
  x: 0,
  y: 0
};

animate({
  from: 0,
  to: 1,
  duration: 2000,
  repeat: Infinity,
  onUpdate: (latest) => {
    state.x = latest;
    state.y = 1 - latest;
  }
});

WebGL-рендеринг привязывается к этому состоянию:

function render(gl, program) {
  gl.clear(gl.COLOR_BUFFER_BIT);

  gl.uniform2f(
    gl.getUniformLocation(program, 'uPosition'),
    state.x,
    state.y
  );

  gl.drawArrays(gl.TRIANGLES, 0, 6);

  requestAnimationFrame(() => render(gl, program));
}

render(gl, program);

Разделение анимационной и графической ответственности

Popmotion не должен напрямую управлять WebGL API. Его роль — вычисление значений.

WebGL-слой отвечает за:

  • создание и управление буферами (ARRAY_BUFFER, ELEMENT_ARRAY_BUFFER)
  • обновление uniform-переменных
  • переключение шейдерных программ
  • управление текстурами и фреймбуферами

Popmotion отвечает за:

  • интерполяцию значений
  • физические модели движения (spring, inertia)
  • временную ось анимации
  • композицию нескольких анимаций

Преобразование потоков Popmotion в uniform-параметры

WebGL-шейдеры требуют компактных числовых входов. Popmotion генерирует высокоуровневые значения, которые необходимо нормализовать.

Пример: позиционирование объекта

animate({
  from: { x: -1, y: -1 },
  to: { x: 1, y: 1 },
  duration: 1500,
  onUpdate: (v) => {
    gl.uniform2f(
      gl.getUniformLocation(program, 'uPosition'),
      v.x,
      v.y
    );
  }
});

Шейдер:

attribute vec2 position;
uniform vec2 uPosition;

void main() {
  gl_Position = vec4(position + uPosition, 0.0, 1.0);
}

Согласование координатных систем

WebGL использует normalized device coordinates (NDC):

  • X: -1 до 1
  • Y: -1 до 1

Popmotion работает в абстрактных единицах.

Проблема несоответствия

Без нормализации анимации будут выглядеть некорректно: смещения окажутся чрезмерными или слишком малыми.

Решение: слой трансформации

function toNDC(value, min, max) {
  return ((value - min) / (max - min)) * 2 - 1;
}

Использование:

animate({
  from: 0,
  to: 300,
  onUpdate: (v) => {
    const x = toNDC(v, 0, 300);

    gl.uniform1f(
      gl.getUniformLocation(program, 'uX'),
      x
    );
  }
});

Интеграция spring-моделей с GPU-пайплайном

Popmotion предоставляет физически-основанные анимации, особенно spring-движение. Это критично для WebGL-интерактивности (перетаскивание, инерция камеры).

Пример spring-анимации камеры

import { spring } from 'popmotion';

let camera = { x: 0, y: 0 };

spring({
  from: 0,
  to: 100,
  stiffness: 120,
  damping: 20,
  onUpdate: (v) => {
    camera.x = v;
  }
});

Передача в WebGL:

function render() {
  gl.uniform2f(
    gl.getUniformLocation(program, 'uCamera'),
    camera.x,
    camera.y
  );

  gl.drawArrays(gl.TRIANGLES, 0, vertexCount);
  requestAnimationFrame(render);
}

Обновление vertex buffer через Popmotion

При динамической геометрии Popmotion может управлять массивами вершин.

Пример деформации сетки

const vertices = new Float32Array([
  -0.5, -0.5,
   0.5, -0.5,
   0.5,  0.5,
  -0.5,  0.5
]);

animate({
  from: 0,
  to: Math.PI * 2,
  duration: 3000,
  repeat: Infinity,
  onUpdate: (t) => {
    for (let i = 0; i < vertices.length; i += 2) {
      vertices[i + 1] += Math.sin(t + vertices[i]) * 0.01;
    }

    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.DYNAMIC_DRAW);
  }
});

Батчинг обновлений и контроль производительности

WebGL чувствителен к частым bufferData и uniform вызовам. Popmotion может генерировать обновления с высокой частотой, поэтому требуется контроль.

Стратегия throttling через RAF

let dirty = false;

animate({
  from: 0,
  to: 1,
  onUpdate: () => {
    dirty = true;
  }
});

function render() {
  if (dirty) {
    updateUniforms();
    drawScene();
    dirty = false;
  }

  requestAnimationFrame(render);
}

Координация нескольких анимационных потоков

Popmotion позволяет запускать несколько независимых анимаций. В WebGL это приводит к необходимости синхронизации состояний.

Пример: объект + свет + камера

const state = {
  objectX: 0,
  lightX: 0,
  cameraZoom: 1
};

animate({
  from: 0,
  to: 1,
  onUpdate: (v) => state.objectX = v
});

animate({
  from: 1,
  to: 0,
  onUpdate: (v) => state.lightX = v
});

В рендере:

function render() {
  gl.uniform1f(uObjectX, state.objectX);
  gl.uniform1f(uLightX, state.lightX);
  gl.uniform1f(uZoom, state.cameraZoom);

  gl.drawArrays(gl.TRIANGLES, 0, count);
  requestAnimationFrame(render);
}

Работа с pointer-интерактивностью и WebGL сценой

Popmotion часто используется для обработки drag/gesture логики, которая затем передаётся в WebGL как трансформации.

Перетаскивание объекта

import { pointer, transform } from 'popmotion';

const offset = { x: 0, y: 0 };

pointer({
  onMove: (p) => {
    offset.x = transform.getValueFromProgress(-1, 1, p.x / window.innerWidth);
    offset.y = transform.getValueFromProgress(-1, 1, p.y / window.innerHeight);
  }
});

WebGL:

gl.uniform2f(uOffset, offset.x, offset.y);

Синхронизация времени Popmotion и WebGL

Popmotion использует внутренний таймер, основанный на performance.now(). WebGL-цикл обычно привязан к requestAnimationFrame.

Проблема возникает при:

  • пропуске кадров
  • вкладке background
  • throttling браузера

Решение: единый time source

let time = 0;

animate({
  from: 0,
  to: 1000,
  duration: Infinity,
  onUpdate: (t) => {
    time = t;
  }
});

function render(now) {
  gl.uniform1f(uTime, time);

  draw();
  requestAnimationFrame(render);
}

Интеграция easing-кривых в shader-параметры

Popmotion easing может быть использован не только для позиционирования, но и для управления визуальными эффектами WebGL:

  • прозрачность
  • blur через sampling
  • displacement map strength
  • wave distortion

Пример: синусоидальная деформация

animate({
  from: 0,
  to: 1,
  duration: 2000,
  onUpdate: (v) => {
    gl.uniform1f(uStrength, v);
  }
});

Shader:

float offset = sin(position.x * 10.0 + uTime) * uStrength;
gl_Position.y += offset;

Паттерн архитектуры: Popmotion как state engine для WebGL

Оптимальная модель разделения:

1. Animation Layer (Popmotion)

  • вычисляет state
  • управляет временными функциями
  • не знает о WebGL

2. Render Layer (WebGL)

  • читает state
  • обновляет GPU
  • выполняет draw calls

3. Bridge Layer

  • минимальный слой синхронизации
  • хранит shared state объект
  • гарантирует consistency per frame
const bridge = {
  state: {},
  update(key, value) {
    this.state[key] = value;
  }
};

Проблема дрейфа состояния и её устранение

При длительных анимациях возможно накопление ошибок из-за:

  • floating point precision
  • несинхронных обновлений
  • разной частоты потоков

Решение:

  • всегда использовать Popmotion как source of truth
  • не модифицировать state в WebGL-цикле
  • пересчитывать state при необходимости

Работа с instancing и Popmotion

При использовании ANGLE_instanced_arrays Popmotion может управлять массивами трансформаций:

animate({
  from: 0,
  to: 1,
  onUpdate: (v) => {
    for (let i = 0; i < instanceData.length; i++) {
      instanceData[i] = v * i;
    }

    gl.bufferSubData(gl.ARRAY_BUFFER, 0, instanceData);
  }
});

Координация сложных сцен с несколькими shader programs

При наличии нескольких WebGL программ Popmotion должен обновлять state централизованно:

  • одна анимация → несколько uniform targets
  • единый источник времени
  • разделение по namespace
const uniforms = {
  sceneA: { uTime, uIntensity },
  sceneB: { uTime, uOffset }
};

animate({
  from: 0,
  to: 1,
  onUpdate: (v) => {
    gl.uniform1f(uniforms.sceneA.uIntensity, v);
    gl.uniform1f(uniforms.sceneB.uOffset, 1 - v);
  }
});