Анимация шейдеров

Шейдерная анимация в WebGL строится вокруг управления параметрами GPU-программы — uniform-переменными, которые передаются в GLSL-код из JavaScript. Popmotion выступает как слой временной и физической анимации, преобразующий дискретные изменения состояния в непрерывные кривые движения.

Основная идея заключается в разделении ответственности:

  • GLSL отвечает за вычисления пикселей
  • WebGL контекст управляет отрисовкой
  • Popmotion управляет временной динамикой значений uniform-переменных

Такой подход позволяет отказаться от ручного requestAnimationFrame-контроля анимации и передать управление кривыми, инерцией и интерполяцией специализированному движку.


Базовая архитектура связки Popmotion + WebGL

Типичная схема взаимодействия выглядит следующим образом:

  1. Создаётся WebGL программа с вершинным и фрагментным шейдером
  2. Определяются uniform-переменные (время, интенсивность, смещения)
  3. Popmotion анимирует JavaScript-объект состояния
  4. На каждом тике Popmotion обновляет uniforms
  5. WebGL перерисовывает сцену

Ключевая особенность — Popmotion не знает о WebGL, он работает только с данными.


Пример минимального шейдера

Фрагментный шейдер, реагирующий на время и амплитуду:

precision mediump float;

uniform float u_time;
uniform float u_amplitude;
uniform vec2 u_resolution;

void main() {
    vec2 uv = gl_FragCoord.xy / u_resolution;

    float wave = sin(uv.x * 10.0 + u_time) * u_amplitude;
    float intensity = 0.5 + 0.5 * wave;

    gl_FragColor = vec4(vec3(intensity), 1.0);
}

Здесь вся анимация завязана на u_time и u_amplitude, которые будут управляться из JavaScript.


Привязка uniform-переменных к Popmotion

Popmotion позволяет анимировать объект состояния:

import { animate } from 'popmotion';

const uniforms = {
  u_amplitude: 0,
  u_time: 0
};

Далее запускается анимация:

animate({
  from: 0,
  to: 1,
  duration: 2000,
  onUpdate: v => {
    uniforms.u_amplitude = v;
  }
});

Каждое обновление значения v синхронизируется с uniform-переменной.


Интеграция с render loop WebGL

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

function render(time) {
  uniforms.u_time = time * 0.001;

  gl.uniform1f(locations.u_time, uniforms.u_time);
  gl.uniform1f(locations.u_amplitude, uniforms.u_amplitude);

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

  requestAnimationFrame(render);
}

requestAnimationFrame(render);

Popmotion отвечает за изменение параметров, а render loop — за их применение.


Использование физической модели анимации

Popmotion поддерживает физические модели, которые особенно полезны для шейдерной динамики.

spring-анимация для плавных переходов

import { spring } from 'popmotion';

spring({
  from: 0,
  to: 1,
  stiffness: 80,
  damping: 12,
  onUpdate: v => {
    uniforms.u_amplitude = v;
  }
}).start();

Физическая модель позволяет добиться эффекта «живого» шейдера, где волны и искажения реагируют инерционно.


Связь пользовательского ввода и шейдеров

Шейдерная анимация часто управляется мышью или касаниями.

window.addEventListener('mousemove', (e) => {
  const x = e.clientX / window.innerWidth;
  const y = e.clientY / window.innerHeight;

  uniforms.u_mouseX = x;
  uniforms.u_mouseY = y;
});

В шейдере:

uniform vec2 u_mouse;

float dist = distance(uv, u_mouse);
float ripple = sin(dist * 20.0 - u_time * 3.0);

Popmotion в таком случае может сглаживать движение мыши:

import { animate } from 'popmotion';

animate({
  from: { x: 0, y: 0 },
  to: { x: targetX, y: targetY },
  onUpdate: v => {
    uniforms.u_mouseX = v.x;
    uniforms.u_mouseY = v.y;
  }
});

Анимация цветовых пространств

Шейдеры часто используют HSL/HSV преобразования. Popmotion может анимировать компоненты цвета независимо.

GLSL:

vec3 colorA = vec3(0.2, 0.6, 1.0);
vec3 colorB = vec3(1.0, 0.3, 0.5);

vec3 color = mix(colorA, colorB, u_mix);

Jav * aScript:

animate({
  from: 0,
  to: 1,
  duration: 3000,
  repeat: Infinity,
  onUpdate: v => {
    uniforms.u_mix = v;
  }
});

Это создаёт непрерывную морфинг-анимацию между двумя цветовыми состояниями.


Многослойная шейдерная анимация

Сложные эффекты строятся через композицию нескольких uniform-каналов:

  • u_time — глобальное время
  • u_noiseStrength — интенсивность шума
  • u_distortion — искажение UV
  • u_colorMix — смешивание палитр

Popmotion позволяет анимировать их независимо:

animate({
  from: 0,
  to: 1,
  duration: 1500,
  onUpdate: v => uniforms.u_noiseStrength = v
});

animate({
  from: 1,
  to: 0,
  duration: 3000,
  repeat: Infinity,
  onUpdate: v => uniforms.u_distortion = v
});

Так создаётся многослойная динамика, где разные аспекты шейдера живут собственной временной линией.


Использование keyframes для сложных состояний

Popmotion поддерживает последовательности значений:

import { keyframes } from 'popmotion';

keyframes({
  values: [0, 1, 0.5, 1],
  times: [0, 0.3, 0.6, 1],
  duration: 4000,
  onUpdate: v => {
    uniforms.u_amplitude = v;
  }
}).start();

Это позволяет задавать нелинейные паттерны поведения шейдеров, включая пульсации, всплески и затухающие волны.


Оптимизация передачи данных в GPU

При высокой частоте обновлений важно минимизировать количество вызовов gl.uniform*.

Практика:

  • объединение uniforms в структуры состояния
  • обновление только изменившихся значений
  • использование буферов (VBO) для сложных данных

Пример паттерна:

const state = {
  amplitude: 0,
  mix: 0,
  distortion: 0
};

function syncUniforms() {
  gl.uniform1f(locations.u_amplitude, state.amplitude);
  gl.uniform1f(locations.u_mix, state.mix);
  gl.uniform1f(locations.u_distortion, state.distortion);
}

Popmotion обновляет state, WebGL синхронизирует GPU.


Шумовые функции и динамическая анимация

Шейдерная анимация часто опирается на noise-функции:

float noise(vec2 p) {
    return fract(sin(dot(p, vec2(12.9898,78.233))) * 43758.5453);
}

Popmotion может управлять параметрами шума:

animate({
  from: 0,
  to: 10,
  duration: 5000,
  onUpdate: v => uniforms.u_noiseScale = v
});

В результате изменяется «зернистость» движения без изменения самой функции шума.


Комбинирование инерции и синусоидальных колебаний

Popmotion spring-модель часто используется совместно с периодическими функциями шейдера:

GLSL:

float wave = sin(u_time * u_speed + uv.x * 5.0);
wave *= u_amplitude;

Jav * aScript:

spring({
  from: 0.2,
  to: 1.0,
  stiffness: 60,
  damping: 15,
  onUpdate: v => uniforms.u_amplitude = v
}).start();

Получается эффект живого отклика системы на изменение параметров.


Синхронизация нескольких шейдерных программ

В сложных сценах используется несколько WebGL программ:

  • фоновые эффекты
  • постобработка
  • интерфейсные слои

Popmotion может управлять глобальным состоянием:

const globalState = {
  intensity: 0,
  blur: 0
};

И затем распределять его по разным шейдерам:

animate({
  from: 0,
  to: 1,
  onUpdate: v => {
    globalState.intensity = v;
    globalState.blur = v * 0.5;
  }
});

Временные деформации и нелинейное время

Шейдеры позволяют модифицировать ощущение времени:

float warpedTime = u_time + sin(u_time) * 0.5;

Popmotion может анимировать коэффициент искажения времени:

animate({
  from: 0,
  to: 2,
  duration: 6000,
  onUpdate: v => uniforms.u_timeWarp = v
});

Это создаёт эффект ускорения и замедления визуальных процессов без изменения реального времени render loop.


Декоративные эффекты через синхронизацию слоёв

Комбинация нескольких анимаций Popmotion позволяет создавать сложные визуальные паттерны:

  • пульсирующие градиенты
  • радиальные волны
  • плавающие шумовые поля
  • морфинг форм

Каждый слой управляется отдельной анимационной кривой, но все они сходятся в одном GPU-проходе, формируя единый визуальный результат.