摘要:輸入結點主要負責加載解碼音頻源,比如獲取二進制音頻源的獲取音頻源的等處理結點主要對音頻數據進行計算處理,比如處理音頻振幅的等輸出結點則將音頻輸出至揚聲器或耳機,便是默認的輸出節點。
在VR開發中,除了圖形視覺渲染,音頻處理是重要的一環,好的音頻處理可以欺騙用戶的聽覺,達到身臨其境的效果,本文主要介紹WebVR音頻是如何開發的。
VR AudioVR音頻的輸出硬件主要是耳機,根據音頻源與場景之間的關系,可將VR音頻分為兩類:靜態音頻和空間化音頻(audio spatialization)。
靜態音頻這類音頻作用于整個VR場景,可簡單的理解成背景音樂,音頻輸出是靜態的,比如微風雨滴聲、鬧市聲等充斥整個場景的背景音效。
對于環境音效的開發,我們可以簡單的使用
音頻作用在空間的實體上,具有發聲體和聽者的位置關系,音頻輸出會根據發聲體與用戶的距離、方向動態變化,它模擬了現實中聲音的傳播方式,具有空間感。
實現原理:在虛擬場景中,通過調節音頻的振幅來描述發聲體與聽者之間的距離,再通過調節左右通道(audio channel)之間的差異,控制左右耳機喇叭輸出,來描述發聲體相對聽者的方位。
從發聲體與用戶兩點間的距離來看,如距離越遠,音頻音量(振幅)應越小;
從發聲體與用戶的方向來看,如發聲體位于聽者左側,則音頻輸出的左聲道應比右聲道音量大。
形如音頻空間化此類稍復雜的音頻的處理,可通過Web Audio API來實現。
Web Audio API 簡介Web Audio API提供了一個功能強大的音頻處理系統,允許我們在瀏覽器中通過js來實時控制處理音頻,比如音頻可視化、音頻混合等。
Web Audio處理流程可以比喻成一個加工廠對聲源的加工,這個加工廠由多個加工模塊AudioNode連接而成,音頻源經過一系列的處理加工后,被輸送至揚聲器。
AudioContext類似于canvas的context上下文環境,它代表了一個audio加工廠控制中心,負責各個audioNode的創建和組合,通過new AudioContext()的方式創建。
AudioNodeAudioNode音頻節點,則是加工廠的加工模塊, 按照功能可分為三類:輸入結點、處理結點、輸出結點。每個結點都擁有connect方法連接下一個節點,將音頻輸出到下一個模塊。
輸入結點主要負責加載解碼音頻源,比如獲取二進制音頻源的BufferSourceNode、獲取
處理結點主要對音頻數據進行計算處理,比如處理音頻振幅的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()創建。
我們需要初始化這兩個對象,并將空間方位信息作為入參動態傳給它們。
設置PannerNodeconst 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); // 播放音頻設置AudioListener
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賦值。
Forward向量沿著鼻子方向指向前,默認是(0,0,-1);
Up向量沿著頭頂方向指向上,默認是(0,1,0)。
在VR場景中,當用戶轉動頭部改變視角時,up向量或forward向量會隨之改變,但兩者始終垂直。
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;WebVR實現音頻角色
在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場景的入門教程
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/90745.html
摘要:在文末,我會附上一個可加載的模型方便學習中文藝術字渲染用原生可以很容易地繪制文字,但是原生提供的文字效果美化功能十分有限。 showImg(https://segmentfault.com/img/bVWYnb?w=900&h=385); WebGL 可以說是 HTML5 技術生態鏈中最為令人振奮的標準之一,它把 Web 帶入了 3D 的時代。 初識 WebGL 先通過幾個使用 Web...
摘要:片元著色器主要處理片元顏色,在這里只是將紋理坐標和紋理對象傳給片元著色器。根據公式分別計算出左右視口的模型視圖投影矩陣,傳給頂點著色器程序,與頂點緩沖區的頂點坐標相乘繪制出最終頂點。 最近WebVR API 1.1已經發布,2.0草案也在擬定中,在我看來,WebVR走向大眾瀏覽器是早晚的事情了,今天本人將對WebVR開發環境和開發流程進行深入介紹。 WebVR與WebVR API 首先...
摘要:通過佩戴的分離式頭顯瀏覽連接在主機端的網頁,現支持的瀏覽器主要是火狐的和設置的谷歌。的概念大概就如此,這次我們將采用的方式來測試我們的場景,現在踏上我們的開發之旅。 showImg(https://segmentfault.com/img/remote/1460000008904026); WebVR即web + VR的體驗方式,我們可以戴著頭顯享受沉浸式的網頁,新的API標準讓我們可...