Łączenie pozycjonowania dźwięku z WebGL

Ilmari Heikkinen

Wprowadzenie

W tym artykule omówię, jak za pomocą funkcji dźwięku przestrzennego w interfejsie Web Audio API dodać dźwięk 3D do scen WebGL. Aby dźwięk był bardziej wiarygodny, pokażę Ci też efekty dźwiękowe, które można uzyskać dzięki interfejsowi Web Audio API. Aby dowiedzieć się więcej o interfejsie Web Audio API, zapoznaj się z artykułem Boris Smus Pierwsze kroki z interfejsem Web Audio API.

Aby uzyskać dźwięk pozycyjny, użyj węzła AudioPannerNode w interfejsie Web Audio API. AudioPannerNode określa pozycję, orientację i prędkość dźwięku. Dodatkowo kontekst audio interfejsu Web Audio API zawiera atrybut listener, który umożliwia określenie pozycji, orientacji i prędkości słuchacza. Dzięki tym dwóm elementom możesz tworzyć dźwięki kierunkowe za pomocą efektów Dopplera i panoramowania 3D.

Zobaczmy, jak wygląda kod audio powyższej sceny. To jest bardzo podstawowy kod interfejsu Audio API. Utwórz kilka węzłów interfejsu Audio API i połącz je ze sobą. Węzły audio to pojedyncze dźwięki, elementy sterujące głośnością, węzły efektów i analizatory. Po utworzeniu tego schematu musisz go podłączyć do miejsca docelowego kontekstu audio, aby był słyszalny.

// Detect if the audio context is supported.
window.AudioContext = (
  window.AudioContext ||
  window.webkitAudioContext ||
  null
);

if (!AudioContext) {
  throw new Error("AudioContext not supported!");
} 

// Create a new audio context.
var ctx = new AudioContext();

// Create a AudioGainNode to control the main volume.
var mainVolume = ctx.createGain();
// Connect the main volume node to the context destination.
mainVolume.connect(ctx.destination);

// Create an object with a sound source and a volume control.
var sound = {};
sound.source = ctx.createBufferSource();
sound.volume = ctx.createGain();

// Connect the sound source to the volume control.
sound.source.connect(sound.volume);
// Hook up the sound volume control to the main volume.
sound.volume.connect(mainVolume);

// Make the sound source loop.
sound.source.loop = true;

// Load a sound file using an ArrayBuffer XMLHttpRequest.
var request = new XMLHttpRequest();
request.open("GET", soundFileName, true);
request.responseType = "arraybuffer";
request.onload = function(e) {

  // Create a buffer from the response ArrayBuffer.
  ctx.decodeAudioData(this.response, function onSuccess(buffer) {
    sound.buffer = buffer;

    // Make the sound source use the buffer and start playing it.
    sound.source.buffer = sound.buffer;
    sound.source.start(ctx.currentTime);
  }, function onFailure() {
    alert("Decoding the audio buffer failed");
  });
};
request.send();

Pozycja

Dźwięk pozycjonowany wykorzystuje położenie źródeł dźwięku i położenie słuchacza, aby określić, jak miksować dźwięk na głośniki. Źródło dźwięku po lewej stronie słuchacza będzie głośniejsze w lewym głośniku, a na odwrót w przypadku prawej strony.

Aby rozpocząć, utwórz źródło dźwięku i dołącz je do węzła AudioPannerNode. Następnie ustaw pozycję węzła AudioPannerNode. Teraz masz ruchomy dźwięk 3D. Domyślnie pozycja odbiorcy kontekstu audio to (0,0,0), więc gdy jest używana w ten sposób, pozycja węzła AudioPannerNode jest względna do pozycji kamery. Za każdym razem, gdy przesuniesz kamerę, musisz zaktualizować pozycję węzła AudioPannerNode. Aby ustawić pozycję AudioPannerNode względem świata, musisz zmienić pozycję detektora kontekstu audio na pozycję kamery.

Aby skonfigurować śledzenie pozycji, musimy utworzyć węzeł AudioPannerNode i podłączyć go do głównego poziomu głośności.

...
sound.panner = ctx.createPanner();
// Instead of hooking up the volume to the main volume, hook it up to the panner.
sound.volume.connect(sound.panner);
// And hook up the panner to the main volume.
sound.panner.connect(mainVolume);
...

W każdym ujęciu aktualizuj pozycje węzłów AudioPannerNodes. W przykładach poniżej będę używać Three.js.

...
// In the frame handler function, get the object's position.
object.position.set(newX, newY, newZ);
object.updateMatrixWorld();
var p = new THREE.Vector3();
p.setFromMatrixPosition(object.matrixWorld);

// And copy the position over to the sound of the object.
sound.panner.setPosition(p.x, p.y, p.z);
...

Aby śledzić pozycję słuchacza, ustaw pozycję detektora w kontekście audio tak, aby pasowała do pozycji kamery.

...
// Get the camera position.
camera.position.set(newX, newY, newZ);
camera.updateMatrixWorld();
var p = new THREE.Vector3();
p.setFromMatrixPosition(camera.matrixWorld);

// And copy the position over to the listener.
ctx.listener.setPosition(p.x, p.y, p.z);
...

Prędkość

Skoro znamy już położenie słuchacza i AudioPannerNode, przyjrzyjmy się ich prędkościom. Zmiana właściwości prędkości odbiorcy i węzła AudioPannerNode pozwala dodać do efektu Dopplera. Na stronie z przykładami interfejsu Web Audio API znajdziesz kilka ciekawych przykładów efektu Dopplera.

Najprostszym sposobem na uzyskanie informacji o prędkościach dla słuchacza i węzła AudioPannerNode jest śledzenie pozycji poszczególnych klatek. Prędkość detektora to bieżąca pozycja kamery pomniejszona o jej położenie w poprzedniej klatce. Podobnie prędkość węzła AudioPannerNode to jego bieżąca pozycja pomniejszona o jego poprzednią pozycję.

Prędkość można śledzić, uzyskując poprzednią pozycję obiektu, odejmując ją od bieżącej pozycji i dzieląc wynik przez czas upływający od ostatniej klatki. Oto jak to zrobić w Three.js:

...
var dt = secondsSinceLastFrame;

var p = new THREE.Vector3();
p.setFromMatrixPosition(object.matrixWorld);
var px = p.x, py = p.y, pz = p.z;

object.position.set(newX, newY, newZ);
object.updateMatrixWorld();

var q = new THREE.Vector3();
q.setFromMatrixPosition(object.matrixWorld);
var dx = q.x-px, dy = q.y-py, dz = q.z-pz;

sound.panner.setPosition(q.x, q.y, q.z);
sound.panner.setVelocity(dx/dt, dy/dt, dz/dt);
...

Orientacja

Orientacja to kierunek, w którym skierowane jest źródło dźwięku, oraz kierunek, w którym zwrócona jest twarz słuchacza. Dzięki orientacji możesz symulować kierunkowe źródła dźwięku. Weźmy na przykład głośnik kierunkowy. Jeśli stoisz przed głośnikiem, dźwięk będzie głośniejszy niż wtedy, gdy stoisz za głośnikiem. Co ważniejsze, musisz określić orientację słuchacza, aby ustalić, z którego kierunku dobiegają dźwięki. Dźwięk z lewej strony musi się przełączyć na prawą, gdy się odwrócisz.

Aby uzyskać wektor orientacji dla węzła AudioPannerNode, musisz wziąć część obrotu z macierzy modelu obiektu 3D emitującego dźwięk i pomnożyć ją przez wektor vec3(0,0,1), aby zobaczyć, w jakim kierunku jest skierowany. Aby określić orientację kontekstu, musisz uzyskać wektor orientacji kamery. Orientacja słuchacza wymaga też wektora skierowanego w górę, ponieważ musi znać kąt odchylenia głowy słuchacza. Aby obliczyć orientację słuchacza, pobierz część obrotu z macierzy widoku kamery i pomnóż ją przez wektor vec3(0,0,1) dla orientacji oraz wektor vec3(0,-1,0) dla wektora skierowanego w górę.

Aby orientacja miała wpływ na dźwięki, musisz też zdefiniować stożek dźwięku. Kształt stożka dźwięku ma kąt wewnętrzny, kąt zewnętrzny i wzmocnienie zewnętrzne. Dźwięk jest odtwarzany z normalną głośnością wewnątrz kąta wewnętrznego i stopniowo zmienia się na wzmocnienie zewnętrzne w miarę zbliżania się do kąta zewnętrznego. Poza zewnętrznym kątem dźwięk jest odtwarzany z zewnętrznym wzmocnieniem.

Śledzenie orientacji w Three.js jest nieco bardziej skomplikowane, ponieważ wymaga wykonania kilku obliczeń wektorowych i ustawienia na 0 części przekształcenia w macierz świata 4 x 4. Nadal nie ma zbyt wielu linii kodu.

...
var vec = new THREE.Vector3(0,0,1);
var m = object.matrixWorld;

// Save the translation column and zero it.
var mx = m.elements[12], my = m.elements[13], mz = m.elements[14];
m.elements[12] = m.elements[13] = m.elements[14] = 0;

// Multiply the 0,0,1 vector by the world matrix and normalize the result.
vec.applyProjection(m);
vec.normalize();

sound.panner.setOrientation(vec.x, vec.y, vec.z);

// Restore the translation column.
m.elements[12] = mx;
m.elements[13] = my;
m.elements[14] = mz;
...

Śledzenie orientacji kamery wymaga również wektora w górę, więc musisz pomnożyć wektor w górę przez macierz przekształcenia.

...
// The camera's world matrix is named "matrix".
var m = camera.matrix;

var mx = m.elements[12], my = m.elements[13], mz = m.elements[14];
m.elements[12] = m.elements[13] = m.elements[14] = 0;

// Multiply the orientation vector by the world matrix of the camera.
var vec = new THREE.Vector3(0,0,1);
vec.applyProjection(m);
vec.normalize();

// Multiply the up vector by the world matrix.
var up = new THREE.Vector3(0,-1,0);
up.applyProjection(m);
up.normalize();

// Set the orientation and the up-vector for the listener.
ctx.listener.setOrientation(vec.x, vec.y, vec.z, up.x, up.y, up.z);

m.elements[12] = mx;
m.elements[13] = my;
m.elements[14] = mz;
...

Aby ustawić stożek dźwięku, musisz ustawić odpowiednie właściwości węzła panner. Kąty stożka są podawane w stopniach w zakresie od 0 do 360.

...
sound.panner.coneInnerAngle = innerAngleInDegrees;
sound.panner.coneOuterAngle = outerAngleInDegrees;
sound.panner.coneOuterGain = outerGainFactor;
...

Wszystko razem

Podsumowując, Listener audio context śledzi pozycję, orientację i prędkość kamery, a AudioPannerNodes śledzi pozycje, orientacje i prędkości odpowiednich źródeł dźwięku. Musisz zaktualizować położenie, szybkość i orientację węzłów AudioPannerNodes oraz detektora kontekstu audio w każdej klatce.

Wpływ na środowisko

Po skonfigurowaniu dźwięku pozycyjnego możesz ustawić efekty dźwiękowe związane z otoczeniem, aby zwiększyć realizm sceny 3D. Załóżmy, że Twoja scena rozgrywa się w dużej katedrze. W ustawieniach domyślnych dźwięki w scenie brzmią tak, jakbyś stała na zewnątrz. Ta rozbieżność między obrazem a dźwiękiem powoduje przerwanie immersji i powoduje, że scena staje się mniej imponująca.

Interfejs Web Audio API zawiera funkcję ConvolverNode, która umożliwia ustawianie efektów środowiskowych dźwięku. Dodaj go do wykresu przetwarzania dla źródła dźwięku, aby dostosować dźwięk do ustawień. W internecie można znaleźć próbki odpowiedzi impulsowej, których można używać z ConvolverNodes. Można też tworzyć własne. Jest to trochę niewygodne, ponieważ musisz zarejestrować impulsową reakcję miejsca, które chcesz symulować, ale w razie potrzeby możesz to zrobić.

Korzystanie z ConvolverNodes do dźwięków otoczenia wymaga zmiany przewodów na wykresie przetwarzania dźwięku. Zamiast przekazywać dźwięk bezpośrednio do głównego wolumenu, musisz go przekierować przez węzeł ConvolverNode. Aby kontrolować siłę efektu środowiskowego, należy kierować dźwięk wokół węzła ConvolverNode. Aby móc kontrolować głośność miksowania, do węzła ConvolverNode i zwykłego dźwięku należy dołączyć węzły GainNode.

Ostatni użyty przeze mnie schemat przetwarzania dźwięku zawiera dźwięk z obiektów przechodzący przez GainNode używany jako przepuszczający mikser. Z miksera przekazuję dźwięk do węzła ConvolverNode i innego węzła GainNode, który służy do sterowania głośnością zwykłego dźwięku. Węzeł ConvolverNode jest podłączony do własnego węzła GainNode, aby sterować głośnością zwiniętego sygnału audio. Wyjścia GainNodes są połączone z głównym regulatorem głośności.

...
var ctx = new webkitAudioContext();
var mainVolume = ctx.createGain();

// Create a convolver to apply environmental effects to the audio.
var convolver = ctx.createConvolver();

// Create a mixer that receives sound from the panners.
var mixer = ctx.createGain();

sounds.forEach(function(sound){
  sound.panner.connect(mixer);
});

// Create volume controllers for the plain audio and the convolver.
var plainGain = ctx.createGain();
var convolverGain = ctx.createGain();

// Send audio from the mixer to plainGain and the convolver node.
mixer.connect(plainGain);
mixer.connect(convolver);

// Hook up the convolver to its volume control.
convolver.connect(convolverGain);

// Send audio from the volume controls to the main volume control.
plainGain.connect(mainVolume);
convolverGain.connect(mainVolume);

// Finally, connect the main volume to the audio context's destination.
volume.connect(ctx.destination);
...

Aby ConvolverNode działał, musisz załadować próbkę odpowiedzi impulsowej do bufora i skonfigurować ConvolverNode tak, aby z niej korzystał. Wczytywanie sampla odbywa się tak samo jak w przypadku zwykłych próbek. Poniżej znajdziesz przykład jednej z możliwości:

...
loadBuffer(ctx, "impulseResponseExample.wav", function(buffer){
  convolver.buffer = buffer;
  convolverGain.gain.value = 0.7;
  plainGain.gain.value = 0.3;
})
...
function loadBuffer(ctx, filename, callback) {
  var request = new XMLHttpRequest();
  request.open("GET", soundFileName, true);
  request.responseType = "arraybuffer";
  request.onload = function() {
    // Create a buffer and keep the channels unchanged.
    ctx.decodeAudioData(request.response, callback, function() {
      alert("Decoding the audio buffer failed");
    });
  };
  request.send();
}

Podsumowanie

Z tego artykułu dowiesz się, jak dodać dźwięk pozycjonujący do scen 3D za pomocą interfejsu Web Audio API. Interfejs Web Audio API umożliwia ustawianie pozycji, orientacji i prędkości źródeł dźwięku oraz słuchacza. Ustawiając je tak, aby śledziły obiekty w scenie 3D, możesz tworzyć bogate dźwięki w aplikacjach 3D.

Aby jeszcze bardziej uatrakcyjnie brzmiało dźwiękowe tło, możesz użyć węzła ConvolverNode w interfejsie Web Audio API, aby skonfigurować ogólny dźwięk środowiska. Korzystając z interfejsu Web Audio API, możesz symulować różne efekty i otoczenia, od katedr po zamknięte pomieszczenia.

Pliki referencyjne