Music Equalizer w Three.js
Jednym z zastosowań biblioteki Three.js jest tworzenie wizualizacji muzyki. Dzięki połączeniu Web Audio API z Three.js możemy analizować odtwarzany dźwięk w czasie rzeczywistym i zamieniać go na efektowne animacje 3D. W tym artykule stworzymy aplikację typu Music Equalizer, która:
- odczytuje pliki MP3 z dysku użytkownika,
- analizuje widmo częstotliwości,
- wyświetla 128 animowanych słupków,
- wykorzystuje materiały emitujące światło,
- dodaje efekt Bloom,
- płynnie animuje kamerę.
Jak działa aplikacja?
Cały projekt można podzielić na kilka etapów:- utworzenie sceny Three.js,
- dodanie kamery oraz oświetlenia,
- wygenerowanie słupków equalizera,
- odczyt pliku audio,
- analiza dźwięku przez Web Audio API,
- animacja słupków,
- dodanie efektu Bloom.
Tworzymy scenę Three.js
Na początku przygotowujemy standardową scenę.scene = new THREE.Scene(); scene.fog = new THREE.Fog(0x000000, 70, 170);Dodajemy kamerę:
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(0, 35, 85);oraz renderer:
renderer = new THREE.WebGLRenderer({ antialias:true });
renderer.setSize(window.innerWidth, window.innerHeight);
Na tym etapie otrzymujemy pustą scenę gotową do dalszej pracy.
Dodajemy oświetlenie
Same bryły wyglądałyby bardzo płasko, dlatego wykorzystujemy dwa źródła światła. Światło otoczenia:const ambient = new THREE.AmbientLight(0xffffff, 0.42); scene.add(ambient);Światło punktowe:
const point = new THREE.PointLight(
0xffffff,
1.2
);
point.position.set(0,50,50);
scene.add(point);
AmbientLight rozjaśnia całą scenę, natomiast PointLight nadaje słupkom przestrzenny wygląd.
Aby scena nie wyglądała na pustą, dodajemy prostą płaszczyznę.
const floorGeo = new THREE.PlaneGeometry(300,300);
const floorMat = new THREE.MeshStandardMaterial({
color:0x050505,
metalness:0.25,
roughness:0.75
});
const floor = new THREE.Mesh(
floorGeo,
floorMat
)
floor.rotation.x = -Math.PI/2;
scene.add(floor);
Podłoga pozwala lepiej ocenić wysokość animowanych słupków.
Generujemy słupki equalizera
Najważniejszym elementem aplikacji jest tablica słupków. Najpierw definiujemy ich liczbę:const BAR_COUNT = 128;Tworzymy geometrię:
const geometry =
new THREE.BoxGeometry(
0.85,
1,
0.85
);
Następnie w pętli generujemy wszystkie elementy.
for (let i=0; i<BAR_COUNT; i++) {
const bar = new THREE.Mesh(
geometry,
material
);
bar.position.x = (i - BAR_COUNT/2) * 1.15;
scene.add(bar);
bars.push(bar);
}
Każdy słupek jest osobnym obiektem Mesh.
Kolorowanie słupków
Kolory nie są przypadkowe. Dla każdego słupka wyznaczamy inny odcień HSL.const hue = 190 + i * 1.2;Tworzymy materiał:
const material =
new THREE.MeshStandardMaterial({
color:new THREE.Color(
`hsl(${hue},100%,55%)`
),
emissive:new THREE.Color(
`hsl(${hue},100%,48%)`
)
});
Dzięki temu słupki tworzą płynny gradient kolorystyczny.
Wczytywanie pliku MP3
Plik wybieramy za pomocą standardowego elementu HTML.<input type="file" accept="audio/*">Po wybraniu pliku korzystamy z FileReader.
const reader = new FileReader();
reader.onload = () => {
audio = new Audio(
reader.result
);
};
Otrzymujemy obiekt HTMLAudioElement, który będzie odtwarzał muzykę.
Web Audio API
Najważniejszym elementem całego projektu jest analiza dźwięku. Tworzymy AudioContext:audioCtx = audioCtx || new AudioContext();Następnie tworzymy analizator.
analyser = audioCtx.createAnalyser();Ustawiamy rozmiar FFT.
analyser.fftSize = 256;Oznacza to, że analizator podzieli sygnał na wiele pasm częstotliwości.
Pobieranie widma
Dane przechowywane są w tablicy Uint8Array.
dataArray =
new Uint8Array(
analyser.frequencyBinCount
);
Każda klatka animacji pobiera aktualne wartości.
analyser.getByteFrequencyData(
dataArray
);
Każdy element tablicy zawiera liczbę z zakresu 0 - 255, gdzie 0 oznacza brak dźwięku, a 255 oznacza maksymalną energię danej częstotliwości.
Animacja słupków
Najprostsze rozwiązanie wyglądałoby tak:bar.scale.y = value;Jednak dawałoby bardzo szarpany ruch. Znacznie lepiej zastosować interpolację.
bars[i].scale.y +=
(
height -
bars[i].scale.y
) * 0.22;
Powoduje to płynne dochodzenie do nowej wysokości.
To jedna z najczęściej stosowanych technik animacji w Three.js.
Wyliczanie wysokości
Najpierw normalizujemy dane.value / 255Następnie stosujemy funkcję potęgową.
Math.pow(
value / 255,
1.4
)
Dzięki temu ciche dźwięki są mniej widoczne, natomiast głośniejsze znacznie bardziej eksponowane.
Końcowy wzór wygląda następująco.
const height =
Math.max(
Math.pow(
value/255,
1.4
) * 42,
0.3
);
Przesuwanie słupków
Po zmianie skali środek obiektu nadal znajduje się w punkcie (0,0,0). Dlatego należy przesunąć go do góry. Dzięki temu słupki "wyrastają" z podłogi.bars[i].position.y = bars[i].scale.y / 2;Oprócz wysokości zmieniamy również intensywność świecenia.
bars[i].material.emissiveIntensity = 0.25 + (value/255) * 0.75;Im głośniejszy dźwięk, tym mocniej świeci dany słupek. Efekt wygląda znacznie atrakcyjniej niż zwykła zmiana koloru. Glow uzyskujemy dzięki postprocessingowi. Tworzymy EffectComposer:
composer = new EffectComposer(renderer);Dodajemy RenderPass:
composer.addPass(
new RenderPass(
scene,
camera
)
);
Na końcu dodajemy UnrealBloomPass.
const bloom = new UnrealBloomPass(
new THREE.Vector2(
window.innerWidth,
window.innerHeight
),
0.55,
0.32,
0.52
);
composer.addPass(bloom);
Bloom powoduje delikatne rozlewanie światła wokół świecących elementów.
Animacja kamery
Scena staje się znacznie bardziej dynamiczna dzięki niewielkiemu ruchowi kamery.
camera.position.x =
Math.sin(
performance.now()*0.00015
) * 6;
camera.lookAt(0,12,0);
Renderowanie sceny
Na końcu pętli animacji renderujemy obraz.composer.render();Gdybyśmy nie korzystali z Bloom, wystarczyłoby:
renderer.render(
scene,
camera
);
Zobacz jak to działa: Music Equalizer w Three.js.