Разработка 3D-аудиовизуализатора с помощью Three.js, GSAP и Web Audio API
- воскресенье, 27 июля 2025 г. в 00:00:04
Визуализатор звука, в котором светящийся 3D-шар пульсирует и меняет цвет в такт музыке, а перетаскиваемые панели GSAP плавно перемещаются вокруг него по инерции.
Звук — это волны, зрение — это волны, которые мы видим. Я всегда стремлюсь поймать момент, когда эти волны накладываются друг на друга. Для недавнего задания от сообщества Webflow и GSAP, посвященного плагинам GSAP Draggable и Inertia, я решил развить идею, создав футуристический визуализатор, реагирующий на звук. Идея заключалась в создании научно-фантастического интерфейса "детектора аномалий", который реагирует на музыку в реальном времени, совмещая атмосферные визуальные эффекты со звуком.
Все началось с простого образа в моем воображении: светящаяся оранжево-белая сфера, одиноко стоящая в темной пустоте, шар, пульсирующий в такт музыке. Чтобы закрепить идею, я прогнал ее через Midjourney: "Светящаяся оранжево-белая градиентная сфера, мягкие размытые слои, плавное искажение, темный фон, легкая зернистость пленки, ретро-аналоговая атмосфера, кинематографическое освещение". После нескольких итераций я выбрал подходящий кадр, быстро раскрасил его в Photoshop и использовал этот чистый, светящийся шар в качестве визуальной основы для всей конструкции.
Первоначально проект был создан как заявка на участие в конкурсе Webflow × GSAP Community Challenge (неделя 2: "Перетаскивание и инерция"), который предполагал использование возможностей перетаскивания и инерции GSAP. Этот контекст повлиял на функционал: я сделал экранные панели управления перетаскиваемыми с эффектом инерции и даже придал 3D-шару легкое инерционное движение при "броске". В этой статье я расскажу обо всем процессе — от настройки сцены Three.js и анализа звука с помощью Web Audio API до создания пользовательских шейдеров (shaders) и добавления анимации и интерактивности с помощью GSAP. В конце вы увидите, как код, визуальные эффекты и звук объединяются, чтобы создать иммерсивный аудиовизуализатор.
Для реализации 3D-части я использовал Three.js для создания сцены, содержащей динамическую сферу ("аномалию") и другие визуальные элементы.
Начнем со стандартной настройки Three.js: сцены, камеры и рендерера. Я выбрал перспективную камеру, чтобы получить хороший 3D-вид сферы, и расположил ее немного позади, чтобы объект полностью попадал в кадр.
OrbitControls
используется для обеспечения простого перемещения по орбите вокруг объекта с помощью щелчков мыши и перетаскивания (с некоторым демпфированием (damping) для плавности). Вот упрощенный фрагмент начальной настройки:
// Инициализируем сцену, камеру и рендерер Three.js
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 100);
camera.position.set(0, 0, 10); // камера находится немного позади от источника
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// Добавляем OrbitControls для вращения камеры
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.1;
controls.rotateSpeed = 0.5;
controls.enableZoom = false; // отключаем возможность масштабирования
Затем я создал объект аномалии. Это главная фишка приложения: каркасная сфера с шипами, реагирующая на звук. Three.js предоставляет такие фигуры, как SphereGeometry
или IcosahedronGeometry
, которые можно использовать для создания сферы. Я выбрал IcosahedronGeometry
, потому что она имеет интересный многогранный вид и позволяет легко управлять деталями (благодаря уровню подразделения (subdivision level)). Аномалия фактически состоит из двух перекрывающихся частей:
внешняя каркасная сфера: IcosahedronGeometry
с кастомным ShaderMaterial
, который рисует ее как светящийся каркас. Эта часть искажается в зависимости от музыки (она "вибрирует" и меняется в такт ритму)
внутренняя светящаяся сфера: немного увеличенная SphereGeometry
, нарисованная с помощью полупрозрачного излучающего шейдера (использующего обратную сторону геометрии) для создания ореола или ауры вокруг каркаса. Это придает сфере эффект теплого свечения, напоминающего энергетическое поле
Я также добавил несколько дополнительных визуальных эффектов: поле из мелких частиц, плавающих на заднем плане (для эффекта глубины, как пыль или искры), и едва заметную сетку в пользовательском интерфейсе (подробнее об интерфейсе позже). Фон сцены темный, а фоновое изображение (отредактированное изображение, сгенерированное Midjourney) я разместил за холстом, чтобы создать таинственный инопланетный пейзаж на горизонте. Такое сочетание трехмерных объектов и двумерного фона создает иллюзию голографического изображения на поверхности планеты.
После создания 3D-сцены следующим шагом стала ее реакция на музыку. Здесь на помощь приходит Web Audio API. Пользователь может либо загрузить свой аудиофайл, либо выбрать один из четырех предоставленных треков. При воспроизведении звука мы подключаемся к аудиопотоку и анализируем его частоты в режиме реального времени с помощью AnalyserNode
. AnalyserNode
предоставляет доступ к данным о частотах. Это моментальный снимок звукового спектра (басы, средние и высокие частоты и т.д.) в любой момент времени, который можно использовать для управления анимацией.
Я создал AudioContext
и AnalyserNode
и подключил к ним источник звука. MediaElementSource для передачи в анализатор можно создать из элемента <audio>
. Например:
// Создаем AudioContext и AnalyserNode
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const analyser = audioContext.createAnalyser();
analyser.fftSize = 2048;
analyser.smoothingTimeConstant = 0.8; // немного сглаживаем частоты
// Подключаем источник элемента аудио к анализатору
const audioElement = document.getElementById('audio-player'); // элемент <audio>
const source = audioContext.createMediaElementSource(audioElement);
source.connect(analyser);
analyser.connect(audioContext.destination); // подключаемся к звуковому выходу
Мы устанавливаем fftSize
в значение 2048
. Это означает, что анализатор будет разбивать аудиосигнал на 1024
частотных интервала (frequencyBinCount
составляет половину fftSize
). Мы также устанавливаем smoothingTimeConstant
, чтобы данные были менее скачкообразными от кадра к кадру. Теперь, по мере воспроизведения аудио, мы можем многократно запрашивать данные у анализатора. Метод analyser.getByteFrequencyData(array)
заполняет массив текущими значениями частот (0-255
) по всему спектру. Аналогично, метод getByteTimeDomainData()
возвращает данные об амплитуде сигнала. В цикле анимации я вызываю getByteFrequencyData()
в каждом кадре для получения свежих данных:
const frequencyData = new Uint8Array(analyser.frequencyBinCount);
function animate() {
requestAnimationFrame(animate);
// Обновляем компоненты Three.js и др...
if (analyser) {
analyser.getByteFrequencyData(frequencyData);
// Вычисляем средний уровень громкости на основе частотных данных
let sum = 0;
for (let i = 0; i < frequencyData.length; i++) {
sum += frequencyData[i];
}
const average = sum / frequencyData.length;
let audioLevel = average / 255; // нормализуем к 0.0-1.0
// Применяем чувствительность (из слайдера в UI)
audioLevel *= (sensitivity / 5.0);
// Теперь audioLevel представляет интенсивность музыки (0 = тишина, ~1 = очень громко)
}
// Используем audioLevel для обновления визуальных компонентов...
renderer.render(scene, camera);
}
Я также определил "пиковую частоту" (частотный диапазон с максимальной амплитудой в данный момент) и некоторые другие метрики просто для развлечения, которые я отображаю в интерфейсе (например, доминирующую частоту в Гц, амплитуду и т.д. как "аномальные метрики"). Но самое главное — это audioLevel
— значение, отражающее общую интенсивность музыки. Мы используем его для управления трехмерными визуальными эффектами.
Синхронизация звука с визуальными эффектами: получив audioLevel
, мы можем внедрить его в Three.js. Я передаю это значение в шейдеры как юниформу (uniform) для каждого кадра, а также использую его для настройки некоторых высокоуровневых движений (например, скорости вращения). Кроме того, события воспроизведения/паузы запускают анимации GSAP (например, небольшое приближение камеры при начале воспроизведения, о чем мы поговорим позже). В результате визуальные эффекты синхронизируются с музыкой: более громкие или интенсивные моменты звука заставляют аномалию светиться ярче и сильнее искажаться, в то время как тихие моменты заставляют ее успокаиваться.
Чтобы добиться динамичности аномалии, я использовал в материале собственные шейдеры GLSL. Three.js позволяет писать собственные шейдеры с помощью THREE.ShaderMaterial
, что идеально подходит для этой цели, поскольку обеспечивает детальный контроль над положением вершин и цветами фрагментов. Это может показаться сложным для новичков в шейдерах, но концептуально мы делаем две важные вещи:
Искажение вершин с шумом: мы смещаем вершины сетки сферы с течением времени, заставляя ее колебаться и скакать. Я включил функцию 3D-шума (Simplex noise) в вершинный шейдер — она генерирует плавное псевдослучайное значение для любой трехмерной координаты. Для каждой вершины я вычисляю значение шума на основе ее положения (плюс временной фактор для анимации). Затем я перемещаю вершину вдоль ее нормали на величину, пропорциональную этому шуму. Мы также умножаем значение на наш audioLevel
и на определяемый пользователем коэффициент искажения. По сути, когда музыка интенсивная (высокий audioLevel
), сфера становится более резкой и хаотичной; когда музыка тихая или поставлена на паузу, сфера почти гладкая.
Свечение по Френелю во фрагментном шейдере: чтобы края каркаса светились и затухали реалистично, я использовал эффект Френеля (Fresnel effect) во фрагментном шейдере. Этот эффект делает поверхности более светлыми под углом падения света. Мы вычисляем его, взяв скалярное произведение направления взгляда и нормали вершины – в результате получается значение, малое на краях (углы падения света) и большее на гранях, обращенных непосредственно к камере. Инвертируя и возводя в степень, мы получаем красивое свечение по контуру сферы, усиливающееся к краям. Я также модулирую интенсивность свечения по Френелю с помощью audioLevel
, чтобы свечение пульсировало в такт ритму.
Рассмотрим упрощенную версию кода шейдера для материала внешней каркасной сферы:
const outerMaterial = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0 },
audioLevel: { value: 0 }, // это значение обновляется на каждом кадре
distortion: { value: 1.0 },
color: { value: new THREE.Color(0xff4e42) } // красно-оранжевый базовый цвет
},
wireframe: true,
transparent: true,
vertexShader: `
uniform float time;
uniform float audioLevel;
uniform float distortion;
// Функция шума опущена для простоты
void main() {
// Начинаем с исходной позиции
vec3 pos = position;
// Вычисляем процедурное значение шума для этой вершины (на основе ее положения и времени)
float noise = snoise(pos * 0.5 + vec3(0.0, 0.0, time * 0.3));
// Смещаем вершину вдоль ее нормали
pos += normal * noise * distortion * (1.0 + audioLevel);
// Стандартное преобразование
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`,
fragmentShader: `
uniform vec3 color;
uniform float audioLevel;
varying vec3 vNormal;
varying vec3 vPosition;
void main() {
// Вычисляем свечение по Френелю (зависящее от угла падения света)
vec3 viewDir = normalize(cameraPosition - vPosition);
float fresnel = 1.0 - max(0.0, dot(viewDir, vNormal));
fresnel = pow(fresnel, 2.0 + audioLevel * 2.0);
// Делаем цвет фрагмента ярче по краям и заставляем его пульсировать со временем
float pulse = 0.8 + 0.2 * sin(time * 2.0);
vec3 emissiveColor = color * fresnel * pulse * (1.0 + audioLevel * 0.8);
// Небольшое затухание альфа-канала при высоком звуке (чтобы сделать скачки более эфирными)
float alpha = fresnel * (0.7 - audioLevel * 0.3);
gl_FragColor = vec4(emissiveColor, alpha);
}
`
});
В этом шейдере snoise
— это функция симплексного шума (опущена для простоты), которая возвращает значения от ~-1
до 1
. Вершинный шейдер использует это значение для смещения каждой вершины (pos += normal * noise * …
). Мы умножаем шум на (1.0 + audioLevel)
, чтобы при увеличении audioLevel
увеличивалось смещение. Равномерность искажения регулируется ползунком в пользовательском интерфейсе, поэтому пользователь может вручную настроить общую "остроту" (spikiness). Фрагментный шейдер вычисляет коэффициент Френеля, чтобы заставить края каркаса светиться сильнее. Обратите внимание, как audioLevel
влияет на мощность и итоговую интенсивность цвета — более громкий звук повышает показатель Френеля (более резкое свечение), а также немного увеличивает яркость. Мы также включили легкую пульсацию (sin(time)
) независимо от звука, просто чтобы создать постоянное "дыхательное" движение.
Для внутренней светящейся сферы используется отдельный ShaderMaterial
: по сути, это сфера, нарисованная с THREE.BackSide
(чтобы мы видели внутреннюю поверхность) и Additive Blending для создания размытого ореола. Ее фрагментный шейдер также использует эффект Френеля, но с гораздо более низким значением альфа-канала, поэтому он выглядит как легкая дымка вокруг сферы. Размер внутренней сферы немного больше (примерно 1,2 радиуса внешней сферы), поэтому свечение выходит за пределы каркаса. В сочетании внешний и внутренний шейдеры создают эффект полупрозрачной, наполненной энергией сферы, поверхность которой пульсирует в такт музыке.
Чтобы связать все это воедино, в каждом кадре цикла рендеринга шейдерные юниформы обновляются с учетом текущего времени и уровня звука:
outerMaterial.uniforms.time.value = elapsedTime;
outerMaterial.uniforms.audioLevel.value = audioLevel;
outerMaterial.uniforms.distortion.value = currentDistortion;
glowMaterial.uniforms.time.value = elapsedTime;
glowMaterial.uniforms.audioLevel.value = audioLevel;
В результате получается трехмерный объект, который словно оживает вместе с музыкой: он колеблется, пульсирует и светится синхронно с воспроизводимым треком.
Поскольку визуальные эффекты реагируют на звук, я добавил GSAP для обеспечения плавной анимации и взаимодействия с пользователем. GSAP отлично подходит для создания последовательностей на временной шкале (timeline sequences) и свойств твинов (tween - термин GSAP) с плавными переходами, а также поставляется с плагинами, идеально подходящими для этого проекта: Draggable
для создания пользовательского интерфейса с функцией перетаскивания и InertiaPlugin
для создания эффекта инерции. И что самое приятное, все плагины GSAP теперь абсолютно бесплатны. Ниже перечислены основные способы использования GSAP в проекте:
Вступительная анимация и движение камеры: когда пользователь выбирает трек и нажимает кнопку воспроизведения, запускается короткая последовательность "активации". Это включает в себя появление текста в "терминале" и небольшое приближение камеры к шару, сигнализирующее о том, что система подключена к сети. Движение камеры выполняется с помощью простого GSAP-твина положения камеры. Я определил положение камеры по умолчанию и немного более приближенное положение. При воспроизведении я использую gsap.to()
для интерполяции положения камеры к приближенным координатам, а при паузе/остановке — обратное удаление. GSAP сильно упрощает такую 3D-анимацию свойств:
const defaultCameraPos = { x: 0, y: 0, z: 10 };
const zoomedCameraPos = { x: 0, y: 0, z: 7 }; // перемещаем камеру ближе
function zoomCameraForAudio(zoomIn) {
const target = zoomIn ? zoomedCameraPos : defaultCameraPos;
gsap.to(camera.position, {
x: target.x,
y: target.y,
z: target.z,
duration: 1.5,
ease: "power2.inOut"
});
}
// При запуске аудио
zoomCameraForAudio(true);
// При остановке/окончании аудио
zoomCameraForAudio(false);
Плавное масштабирование добавляет драматизма в начало воспроизведения, вовлекая зрителя в происходящее. Функция power2.inOut
обеспечивает плавное начало и завершение анимации. Я также использовал временную шкалу GSAP для других последовательностей (например, для постепенного исчезновения наложенного текста "Анализ..." через несколько секунд и т.д.), поскольку управление временной шкалой GSAP очень удобно для организации последовательности нескольких анимаций.
Перетаскиваемые панели пользовательского интерфейса: интерфейс содержит несколько компонентов пользовательского интерфейса, наложенных на 3D-холст, например, панель "Anomaly Controls" (с ползунками для скорости вращения, уровня искажения и т.д.), панель "Audio Spectrum Analyzer" (с гистограммой частот и кнопками выбора треков) и панель "System Terminal" (отображение сообщения журнала как в консоли). Чтобы интерфейс был более интересным, я сделал эти панели перетаскиваемыми. С помощью плагина Draggable
от GSAP я просто превратил каждый элемент .panel
в перетаскиваемый объект:
Draggable.create(".panel", {
type: "x,y",
bounds: "body", // ограничиваем перетаскивание областью просмотра
inertia: true, // включаем импульс после освобождения/завершения перетаскивания
edgeResistance: 0.65, // небольшое сопротивление по краям
onDragStart: () => { /* перемещаем панель на передний план и др. */ },
onDragEnd: function() {
// Опционально, выводим скорость или другую информацию для развлечения
console.log("Panel thrown with velocity:", this.getVelocity());
}
});
При установке inertia: true
, когда пользователь отпускает панель, она продолжает движение в направлении, в котором была брошена, постепенно замедляясь до полной остановки (благодаря InertiaPlugin
). Этот небольшой штрих делает пользовательский интерфейс более осязаемым и реалистичным — вы можете перетаскивать панели, и они будут скользить с некоторым "весом". Согласно документации GSAP, Draggable
автоматически обрабатывает физику, когда включена инерция. Я также ограничил перетаскивание областью просмотра, чтобы панели не терялись за экраном. У каждой панели есть кликабельная часть заголовка (область перетаскивания). Под капотом InertiaPlugin
рассчитывает скорость перетаскивания и создает анимацию, которая плавно замедляет элемент после того, как вы его отпустите, имитируя трение.
Интерактивное перетаскивание сферы (бонус): в качестве творческого эксперимента я также сделал саму сферу аномалии перетаскиваемой. Это было немного сложнее, поскольку она не является элементом DOM, но я реализовал это с помощью raycasting для щелчков мыши по 3D-объекту и последующего вращения объекта на основе движения мыши. Я вручную применил похожий эффект инерции: когда вы "бросаете" сферу, она продолжает вращаться и медленно останавливается. Я не использовал Draggable
GSAP напрямую (так как он работает в экранном пространстве), но использовал концепцию InertiaPlugin
, захватывая скорость перетаскивания, а затем применяя инерционное затухание к этой скорости в каждом кадре. Это добавило интересный способ взаимодействия с визуализатором — вы можете потянуть сферу и увидеть ее физическую реакцию. Такое пользовательское 3D-перетаскивание выходит за рамки базового руководства, но оно показывает, как можно объединить собственную логику с физическими концепциями GSAP для обогащения пользовательского взаимодействия.
Подводя итог, можно сказать, что GSAP обрабатывает все анимации, не связанные со звуком: движение камеры, перетаскивание панелей и небольшие переходы в пользовательском интерфейсе. Сочетание шейдерных анимаций, реагирующих на звук (запускающихся в каждом кадре на основе аудиоданных), и событийных анимаций GSAP (запускаемых действиями пользователя или в определенные моменты времени) дает многослойный результат, где все кажется отзывчивым и живым.
Наконец, несколько слов о пользовательском интерфейсе и атмосфере, которые связывают все воедино. Стиль визуализатора был вдохновлен панелями управления из научно-фантастических фильмов, поэтому я отталкивался от него:
Панели управления и индикаторы: я создал интерфейс с помощью HTML/CSS, сохранив его минималистичным (только полупрозрачные темные панели со светлым текстом и несколько ползунков/кнопок). Ключевые элементы управления включают скорость вращения шара, разрешение (уровень тесселяции (tessellation) сетки икосаэдра), величину искажения, реактивность звука (масштабирование звукового воздействия) и чувствительность (которая определяет интерпретацию громкости звука). Изменение этих параметров мгновенно влияет на сцену Three.js — например, перетаскивание ползунка "Разрешение" (Resolution) перестраивает геометрию икосаэдра с большим или меньшим количеством треугольников, что является отличным способом увидеть, как шар переходит от грубого к мелкодисперсному. Панель "Анализатор спектра звука" (Audio Spectrum Analyzer) отображает классическую столбчатую диаграмму частот (нарисованную на холсте с использованием данных анализатора), так что вы получаете 2D-визуализацию, сопровождающую 3D-визуализацию. Также имеется терминал в стиле консоли, который регистрирует события (например, "AUDIO ANALYSIS SYSTEM INITIALIZED" или скорость перетаскивания в удобном формате журнала GSAP), чтобы подчеркнуть концепцию высокотехнологичной системы в действии.
Элементы дизайна: чтобы усилить атмосферу научной фантастики, я добавил тонкую сетку, наложенную на весь экран. Это было сделано на чистом CSS – пара повторяющихся линейных градиентов, образующих горизонтальные и вертикальные линии (толщиной 1 пиксель, очень прозрачные) на прозрачном фоне. Это едва заметно, но создает техническую текстуру, особенно на фоне свечения шара. Я также добавил несколько дрейфующих частиц (крошечных точек), медленно плавающих на фоне, реализованных в виде простых div-элементов, анимированных с помощью JavaScript. Они движутся по псевдослучайным орбитам.
Саундтрек: я добавил три атмосферных и меланхоличных трека, а также один из моих собственных, ранее не издававшихся, треков под моим музыкальным псевдонимом LXSTNGHT. Трек был записан в Ableton и еще не закончен. В результате получился проект, в котором дизайн, код и создание музыки сливаются воедино.
Объединяя все эти элементы, мы получаем интерактивное произведение искусства: вы загружаете трек, система "Audio ARK" подключается к сети, выдавая поток текстовых сообщений, начинает играть фоновая музыка, а сфера начинает пульсировать и изменяться синхронно со звуком. Вы можете настраивать элементы управления или перемещать панели (или саму сферу), чтобы исследовать различные визуальные эффекты.
Сочетание Three.js (для рендеринга и шейдерных эффектов), Web Audio API (для анализа звука) и GSAP (для взаимодействия с пользователем) демонстрирует, как креативные инструменты кодирования могут объединяться для создания захватывающего опыта, задействующего несколько органов чувств человека.
На этом у меня все, благодарю за внимание!
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩