本文來自於騰訊bugly開發者社區,非經做者贊成,請勿轉載,原文地址:http://dev.qq.com/topic/57c7f...html
做者:蘇晏燁前端
最近VR的發展十分吸引人們的眼球,不少同窗應該也心癢癢的想體驗VR設備,然而如今的專業硬件價格還比較高,入手一個估計就要吃土了。可是,對於咱們前端開發者來講,咱們不只能夠簡單地在手機上進行視覺上的VR體驗,還能夠立立刻手進行Web端VR應用的開發!node
WebVR是一個實驗性的Javascript API,容許HMD(head-mounted displays)鏈接到web apps,同時可以接受這些設備的位置和動做信息。這讓使用Javascript開發VR應用成爲可能(固然已經有不少接口API讓Javascript做爲開發語言了,不過這並不影響咱們爲WebVR感到興奮)。而讓咱們可以立馬進行預覽與體驗,移動設備上的chrome已經支持了WebVR並使手機做爲一個簡易的HMD。手機能夠把屏幕分紅左右眼視覺並應用手機中的加速度計、陀螺儀等感應器,你須要作的或許就只是買一個cardboard。git
不說了,我去下單了!es6
img cardborad紙盒,一頓食堂飯錢便可入手github
WebVR仍處於w3c的草案階段,因此開發和體驗都須要polyfill。web
這篇解析基於 webvr-boilerplate ,這個示例的做者,任職google的 Boris Smus 同時也編寫了 webvr-polyfill 。 three.js examples中也提供了關於VR的控制例子。這裏主要經過對代碼註釋的方式來解讀關鍵的文件。spring
示例的最終效果以下,打開Demo並把手機放進cardboard便可體驗。你也能夠在個人github對照有關的代碼和註釋。chrome
Demo連接:http://soaanyip.github.io/web...
個人github:https://github.com/SoAanyip/W...canvas
按照慣例,這篇解析默認你至少有three.js相關的基礎知識。有興趣也能夠瀏覽一下我以前寫的
《ThreeJS 輕鬆實現主視覺太陽系漫遊》
https://zhuanlan.zhihu.com/p/...
這篇解析中three.js的版本爲V76。文中若有各類錯誤請指出!
在示例中只用到了一個index.html。首先meta標籤有幾個值得注意的:
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0, shrink-to-fit=no"> <meta name="mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
這幾個標籤對web app開發的同窗來講應該是十分熟悉了。其中 shrink-to-fit=no
是Safari的特性,禁止頁面經過縮放去適應適口。
接下來在js引用的部分,引用了這幾個資源:
//做者引入的一個promise polyfill; <script src="node_modules/es6-promise/dist/es6-promise.js"></script>
//three.js核心庫 <script src="node_modules/three/three.js"></script>
//從鏈接的VR設備中得到位置信息並應用在camera對象上,將在下文展開; <script src="node_modules/three/examples/js/controls/VRControls.js"></script>
//處理立體視覺和繪製相關,將在下文展開; <script src="node_modules/three/examples/js/effects/VREffect.js"></script>
//WebVR polyfill,下文簡述調用的API option; <script src="node_modules/webvr-polyfill/build/webvr-polyfill.js"></script>
// 界面按鈕以及進入/退出VR模式的控制等。 <script src="build/webvr-manager.js"></script>
具體的整個項目文件,能夠在這裏查看有關的代碼和註釋。
這個文件主要對HMD的狀態信息進行獲取並應用到camera上。例如在手機上顯示的時候,手機的旋轉傾斜等就會直接做用到camera上。
第一步是獲取鏈接的VR設備,這一步是基本經過WebVR的API進行的:
//獲取VR設備(做爲信息輸入源。若有多個則只取第一個) function gotVRDevices( devices ) { for ( var i = 0; i < devices.length; i ++ ) { if ( ( 'VRDisplay' in window && devices[ i ] instanceof VRDisplay ) || ( 'PositionSensorVRDevice' in window && devices[ i ] instanceof PositionSensorVRDevice ) ) { vrInput = devices[ i ]; break; // We keep the first we encounter } } if ( !vrInput ) { if ( onError ) onError( 'VR input not available.' ); } } //調用WebVR API獲取VR設備 if ( navigator.getVRDisplays ) { navigator.getVRDisplays().then( gotVRDevices ); } else if ( navigator.getVRDevices ) { // Deprecated API. navigator.getVRDevices().then( gotVRDevices ); }
而後是三個關於位置的參數:
// the Rift SDK returns the position in meters // this scale factor allows the user to define how meters // are converted to scene units. //Rift SDK返回的位置信息是以米做爲單位的。這裏能夠定義以幾倍的縮放比例轉換爲three.js中的長度。 this.scale = 1; // If true will use "standing space" coordinate system where y=0 is the // floor and x=0, z=0 is the center of the room. //表示使用者是否站立姿態。當爲false時camra會在y=0的位置,而爲true時會結合下面的模擬身高來決定camera的y值。 //在沒法獲取用戶姿式信息的設備上,須要在調用時直接指定是站姿仍是坐姿。 this.standing = false; // Distance from the users eyes to the floor in meters. Used when // standing=true but the VRDisplay doesn't provide stageParameters. //當爲站立姿態時,用戶的眼睛(camera)的高度(跟若有硬件時返回的單位一致,爲米)。這裏會受scale的影響。如scale爲2時,實際camera的高度就是3.2。 this.userHeight = 1.6;
經過WebVR API獲取到用戶的設備信息,並應用到camera上,是一個持續進行的過程。所以這部分的信息更新會在requestAnimationFrame中不斷地調用。
//將在requestAnimationFrame中應用更新 this.update = function () { if ( vrInput ) { if ( vrInput.getPose ) { //方法返回傳感器在某一時刻的信息(object)。例如包括時間戳、位置(x,y,z)、線速度、線加速度、角速度、角加速度、方向信息。 var pose = vrInput.getPose(); //orientation 方向 if ( pose.orientation !== null ) { //quaternion 四元數 //把設備的方向複製給camera object.quaternion.fromArray( pose.orientation ); } //位置信息 if ( pose.position !== null ) { //一樣把設備的位置複製給camera object.position.fromArray( pose.position ); } else { object.position.set( 0, 0, 0 ); } } else { // Deprecated API. var state = vrInput.getState(); if ( state.orientation !== null ) { object.quaternion.copy( state.orientation ); } if ( state.position !== null ) { object.position.copy( state.position ); } else { object.position.set( 0, 0, 0 ); } } //TODO 此塊會一直執行 if ( this.standing ) { //若是硬件返回場景信息,則應用硬件返回的數據來進行站姿轉換 if ( vrInput.stageParameters ) { object.updateMatrix(); //sittingToStandingTransform返回一個Matrix4,表示從坐姿到站姿的變換。 standingMatrix.fromArray(vrInput.stageParameters.sittingToStandingTransform); //應用變換到camera。 object.applyMatrix( standingMatrix ); } else { //若是vrInput不提供y高度信息的話使用userHeight做爲高度 object.position.setY( object.position.y + this.userHeight ); } } //使用上面定義的this.scale來縮放camera的位置。 object.position.multiplyScalar( scope.scale ); } };
以上是vrcontrols的關鍵代碼。
VREffect.js主要把屏幕顯示切割爲左右眼所視的屏幕,兩個屏幕所顯示的內容具備必定的差別,使得人的雙目立體視覺能夠把屏幕中的內容看得立體化。這個文件主要的流程以下圖:
首先是對畫布大小進行了設定。其中 renderer.setPixelRatio( 1 );
是防止在retina等屏幕上出現圖像變形等顯示問題。
//初始化或者resize的時候進行。 this.setSize = function ( width, height ) { rendererSize = { width: width, height: height }; //是否VR模式中 if ( isPresenting ) { //getEyeParameters包含了渲染某個眼睛所視的屏幕的信息,例如offset,FOV等 var eyeParamsL = vrHMD.getEyeParameters( 'left' ); //設備像素比 //若設備像素比不爲1時會出現顯示問題。 //https://github.com/mrdoob/three.js/pull/6248 renderer.setPixelRatio( 1 ); if ( isDeprecatedAPI ) { renderer.setSize( eyeParamsL.renderRect.width * 2, eyeParamsL.renderRect.height, false ); } else { renderer.setSize( eyeParamsL.renderWidth * 2, eyeParamsL.renderHeight, false ); } } else { renderer.setPixelRatio( rendererPixelRatio ); renderer.setSize( width, height ); } };
而後是關於全屏模式的設置,這裏跟上面的設定差不遠:
//顯示設備進入全屏顯示模式 function onFullscreenChange () { var wasPresenting = isPresenting; isPresenting = vrHMD !== undefined && ( vrHMD.isPresenting || ( isDeprecatedAPI && document[ fullscreenElement ] instanceof window.HTMLElement ) ); if ( wasPresenting === isPresenting ) { return; } //若是這次事件是進入VR模式 if ( isPresenting ) { rendererPixelRatio = renderer.getPixelRatio(); rendererSize = renderer.getSize(); //getEyeParameters包含了渲染某個眼睛所視的屏幕的信息,例如offset,FOV等 var eyeParamsL = vrHMD.getEyeParameters( 'left' ); var eyeWidth, eyeHeight; if ( isDeprecatedAPI ) { eyeWidth = eyeParamsL.renderRect.width; eyeHeight = eyeParamsL.renderRect.height; } else { eyeWidth = eyeParamsL.renderWidth; eyeHeight = eyeParamsL.renderHeight; } renderer.setPixelRatio( 1 ); renderer.setSize( eyeWidth * 2, eyeHeight, false ); } else { renderer.setPixelRatio( rendererPixelRatio ); renderer.setSize( rendererSize.width, rendererSize.height ); } }
接下來是對錶示左右眼的camera的設定。兩個camera也確定是PerspectiveCamera:
var cameraL = new THREE.PerspectiveCamera(); //左camera顯示layer 1層(即當某個元素只出如今layer 1時,只有cameraL可見。) cameraL.layers.enable( 1 ); var cameraR = new THREE.PerspectiveCamera(); cameraR.layers.enable( 2 );
從WebVR API中獲取關於某個眼睛所視的屏幕的信息:
//getEyeParameters包含了渲染某個眼睛所視的屏幕的信息,例如offset,FOV等 var eyeParamsL = vrHMD.getEyeParameters( 'left' ); var eyeParamsR = vrHMD.getEyeParameters( 'right' ); if ( ! isDeprecatedAPI ) { // represents the offset from the center point between the user's eyes to the center of the eye, measured in meters. //瞳距的偏移 eyeTranslationL.fromArray( eyeParamsL.offset ); eyeTranslationR.fromArray( eyeParamsR.offset ); //represents a field of view defined by 4 different degree values describing the view from a center point. //得到左右眼的FOV eyeFOVL = eyeParamsL.fieldOfView; eyeFOVR = eyeParamsR.fieldOfView; } else { eyeTranslationL.copy( eyeParamsL.eyeTranslation ); eyeTranslationR.copy( eyeParamsR.eyeTranslation ); eyeFOVL = eyeParamsL.recommendedFieldOfView; eyeFOVR = eyeParamsR.recommendedFieldOfView; } if ( Array.isArray( scene ) ) { console.warn( 'THREE.VREffect.render() no longer supports arrays. Use object.layers instead.' ); scene = scene[ 0 ]; }
因爲左右camera的視錐體還沒肯定,須要對得到的FOV信息進行計算來肯定。在涉及透視投影矩陣的部分會比較複雜,因此這裏不展開來講。若是有錯誤請指出:
cameraL.projectionMatrix = fovToProjection( eyeFOVL, true, camera.near, camera.far ); cameraR.projectionMatrix = fovToProjection( eyeFOVR, true, camera.near, camera.far ); //角度弧度的轉換,而後進行後續的計算 function fovToProjection( fov, rightHanded, zNear, zFar ) { //角度轉換爲弧度 如30度轉爲1/6 PI var DEG2RAD = Math.PI / 180.0; var fovPort = { upTan: Math.tan( fov.upDegrees * DEG2RAD ), downTan: Math.tan( fov.downDegrees * DEG2RAD ), leftTan: Math.tan( fov.leftDegrees * DEG2RAD ), rightTan: Math.tan( fov.rightDegrees * DEG2RAD ) }; return fovPortToProjection( fovPort, rightHanded, zNear, zFar ); } //根據從設備得到的FOV以及相機設定的near、far來生成透視投影矩陣 function fovPortToProjection( fov, rightHanded, zNear, zFar ) { //使用右手座標 rightHanded = rightHanded === undefined ? true : rightHanded; zNear = zNear === undefined ? 0.01 : zNear; zFar = zFar === undefined ? 10000.0 : zFar; var handednessScale = rightHanded ? - 1.0 : 1.0; // start with an identity matrix var mobj = new THREE.Matrix4(); var m = mobj.elements; // and with scale/offset info for normalized device coords var scaleAndOffset = fovToNDCScaleOffset( fov ); //創建透視投影矩陣 // X result, map clip edges to [-w,+w] m[ 0 * 4 + 0 ] = scaleAndOffset.scale[ 0 ]; m[ 0 * 4 + 1 ] = 0.0; m[ 0 * 4 + 2 ] = scaleAndOffset.offset[ 0 ] * handednessScale; m[ 0 * 4 + 3 ] = 0.0; // Y result, map clip edges to [-w,+w] // Y offset is negated because this proj matrix transforms from world coords with Y=up, // but the NDC scaling has Y=down (thanks D3D?) //NDC(歸一化設備座標系)是左手座標系 m[ 1 * 4 + 0 ] = 0.0; m[ 1 * 4 + 1 ] = scaleAndOffset.scale[ 1 ]; m[ 1 * 4 + 2 ] = - scaleAndOffset.offset[ 1 ] * handednessScale; m[ 1 * 4 + 3 ] = 0.0; // Z result (up to the app) m[ 2 * 4 + 0 ] = 0.0; m[ 2 * 4 + 1 ] = 0.0; m[ 2 * 4 + 2 ] = zFar / ( zNear - zFar ) * - handednessScale; m[ 2 * 4 + 3 ] = ( zFar * zNear ) / ( zNear - zFar ); // W result (= Z in) m[ 3 * 4 + 0 ] = 0.0; m[ 3 * 4 + 1 ] = 0.0; m[ 3 * 4 + 2 ] = handednessScale; m[ 3 * 4 + 3 ] = 0.0; //轉置矩陣,由於mobj.elements是column-major的 mobj.transpose(); return mobj; } //計算線性插值信息 function fovToNDCScaleOffset( fov ) { var pxscale = 2.0 / ( fov.leftTan + fov.rightTan ); var pxoffset = ( fov.leftTan - fov.rightTan ) * pxscale * 0.5; var pyscale = 2.0 / ( fov.upTan + fov.downTan ); var pyoffset = ( fov.upTan - fov.downTan ) * pyscale * 0.5; return { scale: [ pxscale, pyscale ], offset: [ pxoffset, pyoffset ] }; }
以後是肯定左右camera的位置和方向。因爲左右眼(左右camera)確定是在頭部(主camera,位置和方向由HMD返回的信息肯定)上的,在咱們得到把眼睛從頭部飛出去的超能力以前,左右camera的位置和方向都是根據主camera來設定的。
//使主camera的位移、旋轉、縮放變換分解,做用到左camra 右camera上。 camera.matrixWorld.decompose( cameraL.position, cameraL.quaternion, cameraL.scale ); camera.matrixWorld.decompose( cameraR.position, cameraR.quaternion, cameraR.scale ); var scale = this.scale; //左右眼camera根據瞳距進行位移。 cameraL.translateOnAxis( eyeTranslationL, scale ); cameraR.translateOnAxis( eyeTranslationR, scale );
最後即是對兩個區域進行渲染。
// 渲染左眼視覺 renderer.setViewport( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); renderer.setScissor( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); renderer.render( scene, cameraL ); // 渲染右眼視覺 renderer.setViewport( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); renderer.setScissor( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); renderer.render( scene, cameraR );
VREffect文件的關鍵點差很少是上述這些。
webvr-polyfill.js
根據WebVR API的草案來實現了一套polyfill。例如根據所處環境是pc仍是手機來肯定使用的是 CardboardVRDisplay
仍是 MouseKeyboardVRDisplay
,在手機環境下的話使用 Device API
來處理手機旋轉、方向等參數的獲取。此外做者還順便作了幾個提示圖標和畫面來優化體驗。在這裏咱們來看一下其API參數:
WebVRConfig = { /** * webvr-polyfill configuration */ // Flag to disabled the UI in VR Mode. //是否禁用VR模式的UI。 CARDBOARD_UI_DISABLED: false, // Default: false // Forces availability of VR mode. //是否強制使VR模式可用。 //FORCE_ENABLE_VR: true, // Default: false. // Complementary filter coefficient. 0 for accelerometer, 1 for gyro. //互補濾波係數。加速度計在靜止的時候是很準的,但運動時的角度噪聲很大,陀螺儀反之。 //互補濾波器徘徊在信任陀螺儀和加速度計的邊界。首先選擇一個時間常數,而後用它來計算濾波器係數。 //例如陀螺儀的漂移是每秒2度,則可能須要一個少於一秒的時間常數去保證在每個方向上的漂移不會超過2度。 //可是當時間常數越低,越多加速度計的噪聲將容許經過。因此這是一個權衡的內容。 //K_FILTER: 0.98, // Default: 0.98. // Flag to disable the instructions to rotate your device. //是否禁用旋轉設備的提示(橫放手機以進入全屏)。 ROTATE_INSTRUCTIONS_DISABLED: false, // Default: false // How far into the future to predict during fast motion. //因爲有給定的方向以及陀螺儀信息,選擇容許預測多長時間以內的設備方向,在設備快速移動的狀況下可讓渲染比較流暢。 //PREDICTION_TIME_S: 0.040, // Default: 0.040 (in seconds). // Flag to disable touch panner. In case you have your own touch controls、 //是否禁用提供的觸摸控制,當你有本身的觸摸控制方式時能夠禁用 //TOUCH_PANNER_DISABLED: true, // Default: false. // To disable keyboard and mouse controls, if you want to use your own // implementation. //是否禁用pc下的鼠標、鍵盤控制。同上。 //MOUSE_KEYBOARD_CONTROLS_DISABLED: true, // Default: false. // Enable yaw panning only, disabling roll and pitch. This can be useful for // panoramas with nothing interesting above or below. // 僅關心左右角度變化,忽略上下和傾斜等。 // YAW_ONLY: true, // Default: false. // Prevent the polyfill from initializing immediately. Requires the app // to call InitializeWebVRPolyfill() before it can be used. //是否阻止組件直接進行初始化構建。若是爲true則須要本身調用InitializeWebVRPolyfill()。 //DEFER_INITIALIZATION: true, // Default: false. // Enable the deprecated version of the API (navigator.getVRDevices). //容許使用過期版本的API。 //ENABLE_DEPRECATED_API: true, // Default: false. // Scales the recommended buffer size reported by WebVR, which can improve // performance. Making this very small can lower the effective resolution of // your scene. //在VR顯示模式下對WebVR推薦的屏幕比例進行縮放。在IOS下若是不爲0.5會出現顯示問題,查看 //https://github.com/borismus/webvr-polyfill/pull/106 BUFFER_SCALE: 0.5, // default: 1.0 // Allow VRDisplay.submitFrame to change gl bindings, which is more // efficient if the application code will re-bind it's resources on the // next frame anyway. // Dirty bindings include: gl.FRAMEBUFFER_BINDING, gl.CURRENT_PROGRAM, // gl.ARRAY_BUFFER_BINDING, gl.ELEMENT_ARRAY_BUFFER_BINDING, // and gl.TEXTURE_BINDING_2D for texture unit 0 // Warning: enabling this might lead to rendering issues. //容許 VRDisplay.submitFrame使用髒矩形渲染。可是開啓此特性可能會出現渲染問題。 //DIRTY_SUBMIT_FRAME_BINDINGS: true // default: false };
其config主要是對一些用戶可選項進行設定。在文件內部,更多的是對 Device API
的應用等等。
在示例的最後是一個顯示簡單的旋轉立方體的demo。此處能夠幫助咱們學習怎麼建立一個WebVR應用。
首先是創建好scene、renderer、camera的三要素:
// Setup three.js WebGL renderer. Note: Antialiasing is a big performance hit. // Only enable it if you actually need to. var renderer = new THREE.WebGLRenderer({antialias: true}); renderer.setPixelRatio(window.devicePixelRatio); // Append the canvas element created by the renderer to document body element. document.body.appendChild(renderer.domElement); // Create a three.js scene. var scene = new THREE.Scene(); // Create a three.js camera. var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
對上面解析過的controls、effect進行調用:
// Apply VR headset positional data to camera. var controls = new THREE.VRControls(camera); //站立姿態 controls.standing = true; // Apply VR stereo rendering to renderer. var effect = new THREE.VREffect(renderer); effect.setSize(window.innerWidth, window.innerHeight); // Create a VR manager helper to enter and exit VR mode. //按鈕和全屏模式管理 var params = { hideButton: false, // Default: false. isUndistorted: false // Default: false. }; var manager = new WebVRManager(renderer, effect, params);
在場景中,添加一個網格顯示的空間,在空間內加入一個小立方體:
// Add a repeating grid as a skybox. var boxSize = 5; var loader = new THREE.TextureLoader(); loader.load('img/box.png', onTextureLoaded); function onTextureLoaded(texture) { texture.wrapS = THREE.RepeatWrapping; texture.wrapT = THREE.RepeatWrapping; texture.repeat.set(boxSize, boxSize); var geometry = new THREE.BoxGeometry(boxSize, boxSize, boxSize); var material = new THREE.MeshBasicMaterial({ map: texture, color: 0x01BE00, side: THREE.BackSide }); // Align the skybox to the floor (which is at y=0). skybox = new THREE.Mesh(geometry, material); skybox.position.y = boxSize/2; scene.add(skybox); // For high end VR devices like Vive and Oculus, take into account the stage // parameters provided. //在高端的設備上,要考慮到設備提供的場景信息的更新。 setupStage(); } // Create 3D objects. var geometry = new THREE.BoxGeometry(0.5, 0.5, 0.5); var material = new THREE.MeshNormalMaterial(); var cube = new THREE.Mesh(geometry, material); // Position cube mesh to be right in front of you. cube.position.set(0, controls.userHeight, -1); scene.add(cube);
最後即是設置requestAnimationFrame的更新。在animate的函數中,不但要考慮立方體的旋轉問題,更重要的是要不斷地獲取HMD返回的信息以及對camera進行更新。
// Request animation frame loop function var lastRender = 0; function animate(timestamp) { var delta = Math.min(timestamp - lastRender, 500); lastRender = timestamp; //立方體的旋轉 cube.rotation.y += delta * 0.0006; // Update VR headset position and apply to camera. //更新獲取HMD的信息 controls.update(); // Render the scene through the manager. //進行camera更新和場景繪製 manager.render(scene, camera, timestamp); requestAnimationFrame(animate); }
以上即是此示例的各個文件的解析。我相信VR的形式除了在遊戲上的應用的前景,在其餘方面也有其值得探索的可行性。因此讓咱們一塊兒來開始WebVR之旅吧!
https://github.com/borismus/webvr-polyfill
https://github.com/borismus/webvr-boilerplate
http://blog.csdn.net/popy007/article/category/640562
http://blog.csdn.net/iispring/article/details/27970937
http://www.idom.me/articles/841.html
更多精彩內容歡迎關注bugly的微信公衆帳號:
騰訊 Bugly是一款專爲移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的狀況以及解決方案。智能合併功能幫助開發同窗把天天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響用戶數最多的崩潰,精準定位功能幫助開發同窗定位到出問題的代碼行,實時上報能夠在發佈後快速的瞭解應用的質量狀況,適配最新的 iOS, Android 官方操做系統,鵝廠的工程師都在使用,快來加入咱們吧!