在VR開發中,除了圖形視覺渲染,音頻處理是重要的一環,好的音頻處理能夠欺騙用戶的聽覺,達到身臨其境的效果,本文主要介紹WebVR音頻是如何開發的。javascript
VR音頻的輸出硬件主要是耳機,根據音頻源與場景之間的關係,可將VR音頻分爲兩類:靜態音頻和空間化音頻(audio spatialization)。html
這類音頻做用於整個VR場景,可簡單的理解成背景音樂,音頻輸出是靜態的,好比微風雨滴聲、鬧市聲等充斥整個場景的背景音效。
對於環境音效的開發,咱們能夠簡單的使用<audio>標籤進行循環播放。java
音頻做用在空間的實體上,具備發聲體和聽者的位置關係,音頻輸出會根據發聲體與用戶的距離、方向動態變化,它模擬了現實中聲音的傳播方式,具備空間感。git
實現原理:在虛擬場景中,經過調節音頻的振幅來描述發聲體與聽者之間的距離,再經過調節左右通道(audio channel)之間的差別,控制左右耳機喇叭輸出,來描述發聲體相對聽者的方位。github
形如音頻空間化此類稍複雜的音頻的處理,可經過Web Audio API來實現。chrome
Web Audio API提供了一個功能強大的音頻處理系統,容許咱們在瀏覽器中經過js來實時控制處理音頻,好比音頻可視化、音頻混合等。canvas
Web Audio處理流程能夠比喻成一個加工廠對聲源的加工,這個加工廠由多個加工模塊AudioNode
鏈接而成,音頻源通過一系列的處理加工後,被輸送至揚聲器。api
相似於canvas
的context
上下文環境,它表明了一個audio加工廠控制中心,負責各個audioNode的建立和組合,經過new AudioContext()
的方式建立。瀏覽器
AudioNode音頻節點,則是加工廠的加工模塊, 按照功能可分爲三類:輸入結點、處理結點、輸出結點。每一個結點都擁有connect
方法鏈接下一個節點,將音頻輸出到下一個模塊。app
BufferSourceNode
、獲取<audio>音頻源的MediaElementSourceNode
等;GainNode
等;AudioContext.destination
即是默認的輸出節點。一個簡單的音頻處理流程只須要分爲四步:
參考如下代碼:
const myAudio = document.querySelector('audio'); const audioCtx = new AudioContext(); // 建立音頻上下文 // 建立輸入結點,解碼audio標籤的音頻源;建立處理結點,處理音頻 const source = audioCtx.createMediaElementSource(myAudio); const gainNode = audioCtx.createGain(); // 建立GainNode結點控制音頻振幅 // 將輸入結點、處理結點、輸出結點兩兩相連 source.connect(gainNode); // 將輸入結點鏈接到gainNode處理結點 gainNode.connect(audioCtx.destination); // 將gainNode鏈接到destination輸出節點 // 經過動態改變結點屬性產生不一樣音效 source.start(0); // 播放音頻 gainNode.gain.value = val; // 設置音量
理解了Web Audio的開發流程,接下來看看如何在WebVR中實現Audio Spatialization,這裏VR場景使用three.js進行開發。
Audio Spatialization的實現主要經過AudioListener
和PannerNode
結點配合,這兩個對象能夠根據空間方位信息動態處理音頻源,並輸出左右聲道。
AudioListener
對象表明三維空間中的聽者(用戶),經過AudioContext.listener
屬性獲取;PannerNode
對象指的是三維空間中的發聲體,經過 AudioContext.createPanner()
建立。咱們須要初始化這兩個對象,並將空間方位信息做爲入參動態傳給它們。
const myAudio = document.querySelector('audio'); const audioCtx = new AudioContext(); // 建立音頻上下文 const source = audioCtx.createMediaElementSource(myAudio); const panner = audioCtx.createPannerNode(); panner.setPosition(speaker.position.x, speaker.position.y, speaker.position.z); // 將發聲體座標傳給PannerNode source.connect(panner); // 將輸入結點鏈接到PannerNode處理結點 panner.connect(audioCtx.destination); source.start(0); // 播放音頻
VR用戶頭顯最多有6-Dof:position位置3-Dof系統和orientation方向3-Dof系統,咱們須要將這6-Dof的信息傳入AudioListener,由它爲咱們處理音頻數據。
對於用戶位置數據,AudioListener提供了三個位置屬性:positionX
,positionY
,positionZ
,它分別表明聽者當前位置的xyz座標,咱們可將用戶在場景中的位置(通常用camera的position)賦值給這三個屬性。
// 爲listener設置position const listener = audioCtx.listener; listener.positionX = camera.position.x; listener.positionY = camera.position.y; listener.positionZ = camera.position.z;
除了傳入用戶的位置,咱們還須要將用戶的視角方向信息傳給AudioListener
,具體是給AudioListener的Forward向量三個份量forwardX
,forwardY
,forwardZ
和Up向量三個份量upX
,upY
,upZ
賦值。
Up向量 = Camera.旋轉矩陣 × [0,1,0]
Forward向量 = Camera.旋轉矩陣 × [0,0,-1]
參照上方公式,這裏的camera是three.js的camera,指代用戶的頭部,經過camera.quaternion
獲取相機的旋轉(四元數)矩陣,與初始向量相乘,獲得當前Up向量和Forward向量,代碼以下:
// 計算當前listener的forward向量 let forward = new THREE.Vector3(0,0,-1); forward.applyQuaternion(camera.quaternion); // forward初始向量與camera四元數矩陣相乘,獲得當前的forward向量 forward.normalize(); // 向量歸一 // 賦值給AudioListener的forward份量 listener.forwardX.value = forward.x; listener.forwardY.value = forward.y; listener.forwardZ.value = forward.z; // 計算當前listener的up向量 let up = new THREE.Vector3(0,1,0); up.applyQuaternion(camera.quaternion); // up初始向量與camera四元數矩陣相乘,獲得當前的up向量 up.normalize(); // 向量歸一 // 賦值給AudioListener的up份量 listener.upX.value = up.x; listener.upY.value = up.y; listener.upZ.value = up.z;
在VR場景中,根據音頻的發起方和接收方能夠分爲兩個角色:Speaker發聲體與Listener聽者,即用戶。
一個VR場景音頻角色由一個Listener和多個Speaker組成,因而筆者將PannerNode
和AudioListener
進行獨立封裝,整合爲Speaker類和Listener對象。
PS:這裏沿用前幾期three.js開發WebVR的方式,可參考《WebVR開發——標準教程》
Speaker類表明發聲體,主要作了如下事情:
update
公用方法,在每一幀中更新PannerNode位置。class Speaker { constructor(ctx,path) { this.path = path; this.ctx = ctx; this.source = ctx.createBufferSource(); this.panner = ctx.createPanner(); this.source.loop = true; // 設置音頻循環播放 this.source.connect(this.panner); // 將輸入結點連至PannerNode this.panner.connect(ctx.destination); // 將PannerNode連至輸出結點 this._processAudio(); // 異步函數,請求與加載音頻數據 } update(position) { const { panner } = this; panner.setPosition(position.x, position.y, position.z); // 將發聲體座標傳給PannerNode } _loadAudio(path) { // 使用fetch請求音頻文件 return fetch(path).then(res => res.arrayBuffer()); } async _processAudio() { const { path, ctx, source } = this; try { const data = await this._loadAudio(path); // 異步請求音頻 const buffer = await ctx.decodeAudioData(data); // 解碼音頻數據 source.buffer = buffer; // 將解碼數據賦值給BufferSourceNode輸入結點 source.start(0); // 播放音頻 } catch(err) { console.err(err); } } }
這裏初始化的流程跟前面略有不一樣,這裏使用的是fetch請求音頻文件,經過BufferSourceNode
輸入結點解析音頻數據。update
方法傳入發聲體position,設置PannerNode
位置。
Listener對象表明聽者,提供update
公用方法,在每幀中傳入AudioListener
的位置和方向。
// 建立Listener對象 const Listener = { init(ctx) { this.ctx = ctx; this.listener = this.ctx.listener; }, update(position,quaternion) { const { listener } = this; listener.positionX = position.x; listener.positionY = position.y; listener.positionZ = position.z; // 計算當前listener的forward向量 let forward = new THREE.Vector3(0,0,-1); forward.applyQuaternion(quaternion); forward.normalize(); listener.forwardX.value = forward.x; listener.forwardY.value = forward.y; listener.forwardZ.value = forward.z; // 計算當前listener的up向量 let up = new THREE.Vector3(0,1,0); up.applyQuaternion(quaternion); up.normalize(); listener.upX.value = up.x; listener.upY.value = up.y; listener.upZ.value = up.z; } }
這裏只是簡單的將AudioListener
做一層封裝,update方法傳入camera的position和四元數矩陣,設置AudioListener
位置、方向。
接下來,將Listener和Speaker引入到WebVR應用中,下面例子描述了這樣一個簡陋場景:一輛狂響喇叭的汽車從你身旁通過,並駛向遠方。
class WebVRApp { ... start() { const { scene, camera } = this; ... // 建立燈光、地面 // 建立一輛簡陋小車 const geometry = new THREE.CubeGeometry(4, 3, 5); const material = new THREE.MeshLambertMaterial({ color: 0xef6500 }); this.car = new THREE.Mesh(geometry, material); this.car.position.set(-12, 2, -100); scene.add(this.car); const ctx = new AudioContext(); // 建立AudioContext上下文 Listener.init(ctx); // 初始化listener this.car_speaker = new Speaker(ctx,'audio/horn.wav'); // 建立speaker,傳入上下文和音頻路徑 } }
首先在start
方法建立小汽車,接着初始化Listener並建立一個Speaker。
class WebVRApp { ... update() { const { scene, camera, renderer} = this; // 啓動渲染 this.car.position.z += 0.4; this.car_speaker.update(this.car.position); // 更新speaker位置 Listener.update(camera.position, camera.quaternion); // 更新Listener位置以及頭部朝向 renderer.render(scene, camera); } } new WebVRApp();
在動畫渲染update
方法中,更新小汽車的位置,並調用Speaker和Listener的update方法,傳入小汽車的位置、用戶的位置和旋轉矩陣,更新音頻空間信息。
示例地址:https://yonechen.github.io/We...(須要支持es7的瀏覽器如新版chrome,太懶沒作打包編譯?)
源碼地址:https://github.com/YoneChen/W...
本文主要講解了WebVR應用音頻空間化的實現步驟,核心是運用了Web Audio API的PannerNode
和AudioListener
兩個對象處理音頻源,文末展現了VR Audio的一個簡單代碼例子,three.js自己也提供了完善的音頻空間化支持,能夠參考PositinalAudio。
最近筆者正在實現WebVR多人聊天室,下期文章圍繞此展開,敬請期待~
更多文章可關注WebVR技術莊園
WebVR開發教程——交互事件(二)使用Gamepad
WebVR開發教程——深度剖析 關於WebVR的開發調試方案以及原理機制
WebVR開發教程——標準入門 使用Three.js開發WebVR場景的入門教程