主要面向Web前端工程師,須要必定Javascript及three.js基礎;
本文主要分享內容爲基於three.js開發WebVR思路及碰到的問題;
有興趣的同窗,歡迎跟帖討論。javascript
目錄:
1、項目體驗
1.一、項目簡介
1.二、功能介紹
1.三、遊戲體驗
2、技術方案
2.一、爲何使用WebVR
2.二、經常使用的WebVR解決方案
2.2.一、Mozilla的A-Frame方案
2.2.二、three.js及webvr-polyfill方案
3、技術實現
3.一、知識儲備
3.二、實現步驟
3.三、工做原理
4、技術難點
4.一、程序與用戶共同控制攝像頭
4.二、多重蒙板貼圖
4.三、鏡頭移動
4.四、3d自適應長度文字提示
4.五、unity3d地形導出
4.六、3dmax動畫導出問題
5、完整的源代碼及相應組件html
1、項目體驗
1.一、項目簡介:
1.1.一、名稱:
「重歷阿爾特里亞」——龍之谷手遊手首發ChinaJoy2016預熱VR小遊戲
前端
1.1.二、開發背景:
基於龍之谷手遊具有的3D屬性,全景視角體驗,以及ChinaJoy首發的線下場景,咱們和品牌討論除了基於VR的線下體驗項目。因爲基於Web技術較好的兼容性、開發的高效性,咱們採用了WebVR技術來實現整個體驗。java
1.1.三、使用WebVR優點:
1.1.3.一、普通web前端工程師能夠參與VR應用開發,下降了開發門檻;
1.1.3.二、跨設備終端、跨操做系統、跨APP載體;
1.1.3.三、開發快速、維護方便、隨時調整、傳播便捷;
1.1.3.四、瀏覽器便可體驗,無需安裝。git
1.二、功能介紹
基於遊戲內3D場景、人物和道具模型,經過WebGL框架three.js開發的VR小遊戲,在ChinaJoy龍之谷手遊展臺給玩家提供線下VR互動體驗,並在後續應用於線上營銷傳播。不具有VR眼鏡設備的用戶可選擇普通模式進行互動體驗。github
1.三、遊戲體驗
若是你身邊正好有VR眼鏡,請選擇VR模式體驗;若是沒有,請選擇普通模式。
須要說明的是,因爲本次應用針對線下場景,而合做方三星提供了最新的S7手機和GearVR設備,因此項目只針對S7作了體驗優化,因此可能部分手機會有卡頓或者3D模型錯亂的狀況。
你能夠掃描以下二維碼或打開http://dn.qq.com/act/vr/進行體驗:
web
2、技術方案
2.一、爲何是時候嘗試WebVR了?
2.1.一、時機慢慢成熟,咱們經過幾件事件便可感知:
2015年初,Mozilla在firefox nightly增長了對WebVR的支持;
2015年末,MozVR團隊推出開源框架A-Frame,能過HTML標籤,便可建立VR網頁;
2015年末,Egret3D發佈,開發團隊稱將在之後版本中實現WebVR的支持;
2016年初,Google與Mozilla聯合建立WebVR標準;
2016年6月,Google計劃將整個Chrome瀏覽器搬進VR世界中。
2.1.二、WebVR開發成本更低。
2015年VR硬件迅速發展,但時至今日,VR內容仍是稍顯單薄。緣由在於,VR開發成本太高,而WebVR依託於WebGL及相似threeJS等框架,大大下降開發者進入VR領域的門檻。
2.1.三、Web自身的優點
上文中已有說起,依託也Web,具備不需安裝、便於傳播、便於快速迭代等特色。json
2.二、目前階段,經常使用的WebVR解決方案:
2.2.一、A-frame
介紹:Mozilla的開源框架,經過定製HTML元素便可構建WebVR方案的框架,適用於沒有webGL與threeJS基礎的初學者。
優勢:基於threeJS的封裝,經過特定的標籤就可以快速建立VR網頁;
缺點:所提供的組件有限,難以完成較複雜的項目。
實例:
2.2.1.一、建立一個簡單的場景。canvas
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="description" content="Composite — A-Frame"> <script src="../aframe.js"></script> </head> <body> <a-scene> <!-- 環境光. --> <a-entity light="type: ambient; color: #888"></a-entity> <a-entity position="0 2.2 4"> <!-- 添加相機 --> <a-entity camera look-controls wasd-controls> <!-- 添加圓環 --> <a-entity cursor geometry="primitive: ring; radiusOuter: 0.015; radiusInner: 0.01; segmentsTheta: 32" material="color: #283644; shader: flat" raycaster="far: 30" position="0 0 -0.75"></a-entity> </a-entity> </a-entity> </a-scene> </body> </html>
源碼講解:
如上簡單的幾個標籤,便可構建一個包含燈光、相機、跟隨相機的物體的場景,其他事情,都將由A-frame進行解析,具體標籤與屬性很少做講解,能夠參考 A-frame DOC。瀏覽器
2.2.1.一、加載一個由軟件(好比3dmax)導出的模型。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="description" content="Composite — A-Frame"> <script src="../aframe.js"></script> <script> AFRAME.registerComponent('json-model', { schema: { type: 'src' }, init: function () { this.loader = new THREE.JSONLoader(); }, update: function () { var mesh = this.el.getOrCreateObject3D('mesh', THREE.Mesh); this.loader.load(this.data, function (geometry) { mesh.geometry = geometry; }); } }); </script> </head> <body> <a-scene> <a-assets> <a-asset-item id="sculpture" src="data/building-ground.js"></a-asset-item> </a-assets> <a-entity id="car" json-model="#sculpture" position="0 0 0" scale="5 5 5" rotation="0 45 0" material="src: url(cross-domain/skin/xianxiasq_zhujianqiangmian_001.png)"></a-entity> </a-scene> </body> </html>
源碼講解:
這個例子主要演示,A-Frame如何添加組件,對,由於A-Frame現階段組件太少,加載自定義模式須要本身擴展組件。而組件添加須要three.js基礎。
so,A-Frame出發點是很是美好的,學習幾個簡單的標籤及屬性,便可以搭建3d/webvr場景,可是現實倒是目前它還並不成熟,而且伴隨着A-Frame主設計師跳槽到Google,因此我很早就放棄這個方案了。
二、基於threeJS與webVR組件,事實上,A-frame就是基於這二者的封裝。
優勢:能夠完成複雜項目,能夠結合原生的webGL;
缺點:須要掌握threeJS,須要瞭解webGL,學習成本較高。
在本項目中,選用的就是這個方案,在下章節中,將會進行詳細介紹。
3、技術實現
3.一、知識儲備:
three.js(掌握)、webGL(瞭解)、javascript
對three.js沒有基礎的同窗,能夠移步至 Three.js實例教程
3.二、實現步驟:
簡單來講,完成一個WebVR應用,須要如下三個步驟:
3.2.一、搭建場景
如上圖與示:
首先咱們須要載入咱們的資源,這些資源包括地形、角色、動畫、及輔助元素;
而後建立咱們須要的元素,好比燈光、相機、天空等;
而後完成主業務邏輯。
3.2.二、交互
即用戶的動做輸入,這些動做包括:
位置移動、旋轉、視線焦點、聲音、甚至全身全部關節動做。
固然,當前咱們可利用的硬件設備有限,手機自身可利用的如陀螺儀、羅盤、聽筒。其他輔助設備經常使用如Leap Motion、Kinect等。
更多的額外設備意識着更高的使用成本,在本案例中使用的到的動做輸入信息:
用戶當前方向,由VRControls.js與webvr-polyfill.js實現完成;
用戶視角焦點,完成按鈕點擊、攻擊等動做,經過跟隨相機的物體檢測碰撞來完成。
3.2.三、分屏
如上圖所示,爲讓用戶更具沉侵感,一般會根據用戶瞳距將屏幕分割成具備必定視差的兩部分,勿需擔憂,這部分工做由VREffect.js來完成。
3.三、工做原理
上節中提到了webvr相關組件,原本咱們能夠簡單利用它提供的接口就能夠完成,但確定仍是有同窗會好奇,它的工做原理是怎樣的呢。
這得從Mozilla與Google 2016年初聯手推出的WebVR API提案開始,WebVR Specification,該提案給VR硬件定義了專門定製的接口,讓開發者可以構建出沉浸感強,溫馨度高的VR體驗。但因爲該標準還處於草案階段,因此咱們開發須要WebVR Polyfill,這個組件不須要特定瀏覽器,就可使用WebVR API中的接口。
因此咱們只須要在項目中,引入webvr-polyfill.js及VRControls、VREffect兩個類,並調用便可。
vrEffect = new THREE.VREffect(renderer); vrControls = new THREE.VRControls(camera);
webvr-polyfill基於普通瀏覽器實現了WebVR API 1.0功能;
VRControls更新攝像頭信息,讓用戶以第一人稱置於場景中;
VREffect負責分屏。
4、技術難點
4.一、程序與用戶共同控制攝像頭
當程序在自動移動鏡頭的過程當中,容許用戶四處觀察,這時候須要一個輔助容器共同控制鏡頭旋轉與移動。
// 添加攝像機 camera = new THREE.PerspectiveCamera(60, size.w / size.h, 1, 10000); camera.position.set(0, 0, 0); camera.lookAt(new THREE.Vector3(0,0,0)); // 輔助鏡頭移動 dolly = dolly = new THREE.Group(); dolly.position.set(10, 40, 40); dolly.rotation.y = Math.PI/10; dolly.add(camera); scene.add(dolly);
4.二、多重蒙板貼圖
如上圖所示,該地形由三種貼圖經過蒙板共同合成,這時候咱們須要使用自定義Shader來實現,由rbg三個通道控制顯示。
核心代碼(片元着色器):
fragmentShader: [ 'uniform sampler2D texture1;', 'uniform sampler2D texture2;', 'uniform sampler2D texture3;', 'uniform sampler2D mask;', 'void main() {', 'vec4 colorTexture1 = texture2D(texture1, vUv* 40.0);', 'vec4 colorTexture2 = texture2D(texture2, vUv* 60.0);', 'vec4 colorTexture3 = texture2D(texture3, vUv* 20.0);', 'vec4 colorMask = texture2D(mask, vUv);', 'vec3 outgoingLight = vec3( colorTexture1.rgb*colorMask.r + colorTexture2.rgb *colorMask.g + colorTexture3.rgb *colorMask.b ) * 0.6;', 'gl_FragColor = vec4(outgoingLight, 1.0);', '}' ].join("\n")
完整代碼(添加three.js燈光,霧化):
// 合成材質 var map1 = texLoader.load('cross-domain/skins/foor_stone02.png' ); var map2 = texLoader.load('cross-domain/skins/green_wet09.png'); var map3 = texLoader.load('cross-domain/skins/stone_dry02.png'); // 自定義複合蒙板shader THREE.FogShader = { uniforms: lib.extend( [ THREE.UniformsLib[ "fog" ], THREE.UniformsLib[ "lights" ], THREE.UniformsLib[ "shadowmap" ], { 'texture1': { type: "t", value: map1}, 'texture2': { type: "t", value: map2}, 'texture3': { type: "t", value: map3}, 'mask': { type: "t", value: texLoader.load('cross-domain/skins/mask.png')} } ] ), vertexShader: [ "varying vec2 vUv;", "varying vec3 vNormal;", "varying vec3 vViewPosition;", THREE.ShaderChunk[ "skinning_pars_vertex" ], THREE.ShaderChunk[ "shadowmap_pars_vertex" ], THREE.ShaderChunk[ "logdepthbuf_pars_vertex" ], "void main() {", THREE.ShaderChunk[ "skinbase_vertex" ], THREE.ShaderChunk[ "skinnormal_vertex" ], "vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );", "vUv = uv;", "vNormal = normalize( normalMatrix * normal );", "vViewPosition = -mvPosition.xyz;", "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", THREE.ShaderChunk[ "logdepthbuf_vertex" ], "}" ].join('\n'), fragmentShader: [ 'uniform sampler2D texture1;', 'uniform sampler2D texture2;', 'uniform sampler2D texture3;', 'uniform sampler2D mask;', 'varying vec2 vUv;', 'varying vec3 vNormal;', 'varying vec3 vViewPosition;', // "vec3 outgoingLight = vec3( 0.0 );", THREE.ShaderChunk[ "common" ], THREE.ShaderChunk[ "shadowmap_pars_fragment" ], THREE.ShaderChunk[ "fog_pars_fragment" ], THREE.ShaderChunk[ "logdepthbuf_pars_fragment" ], 'void main() {', THREE.ShaderChunk[ "logdepthbuf_fragment" ], THREE.ShaderChunk[ "alphatest_fragment" ], 'vec4 colorTexture1 = texture2D(texture1, vUv* 40.0);', 'vec4 colorTexture2 = texture2D(texture2, vUv* 60.0);', 'vec4 colorTexture3 = texture2D(texture3, vUv* 20.0);', 'vec4 colorMask = texture2D(mask, vUv);', 'vec3 normal = normalize( vNormal );', 'vec3 lightDir = normalize( vViewPosition );', 'float dotProduct = max( dot( normal, lightDir ), 0.0 ) + 0.2;', 'vec3 outgoingLight = vec3( colorTexture1.rgb*colorMask.r + colorTexture2.rgb *colorMask.g + colorTexture3.rgb *colorMask.b ) * 0.6;', THREE.ShaderChunk[ "shadowmap_fragment" ], THREE.ShaderChunk[ "linear_to_gamma_fragment" ], THREE.ShaderChunk[ "fog_fragment" ], // 'gl_FragColor = vec4( colorTexture1.rgb*colorMask.r + colorTexture2.rgb *colorMask.g + colorTexture3.rgb *colorMask.b, 1.0 ) + vec4(outgoingLight, 1.0);', // 'gl_FragColor = outgoingLight;', 'gl_FragColor = vec4(outgoingLight, 1.0);', '}' ].join("\n") }; THREE.FogShader.uniforms.texture1.value.wrapS = THREE.FogShader.uniforms.texture1.value.wrapT = THREE.RepeatWrapping; THREE.FogShader.uniforms.texture2.value.wrapS = THREE.FogShader.uniforms.texture2.value.wrapT = THREE.RepeatWrapping; THREE.FogShader.uniforms.texture3.value.wrapS = THREE.FogShader.uniforms.texture3.value.wrapT = THREE.RepeatWrapping; var material = new THREE.ShaderMaterial({ uniforms : THREE.FogShader.uniforms, vertexShader : THREE.FogShader.vertexShader, fragmentShader : THREE.FogShader.fragmentShader, fog: true });
三、 鏡頭移動(依賴Tween類)
功能函數:
cameraTracker: function(paths){ var tweens = []; for(var i = 0; i < paths.length; i++) { (function(i){ var tween = new TWEEN.Tween({pos: 0}).to({pos: 1}, paths[i].duration || 5000); tween.easing(paths[i].easing || TWEEN.Easing.Linear.None); tween.onStart(function(){ var oriPos = dolly.position; var oriRotation = dolly.rotation; this.oriPos = {x: oriPos.x, y: oriPos.y, z: oriPos.z}; this.oriRotation = {x: oriRotation.x, y: oriRotation.y, z: oriRotation.z}; }); tween.onUpdate(paths[i].onupdate || function(){ if(paths[i].pos) { dolly.position.x = this.oriPos.x + this.pos * (paths[i].pos.x - this.oriPos.x); dolly.position.y = this.oriPos.y + this.pos * (paths[i].pos.y - this.oriPos.y); dolly.position.z = this.oriPos.z + this.pos * (paths[i].pos.z - this.oriPos.z); } if(paths[i].rotation) { dolly.rotation.x = this.oriRotation.x + this.pos * (paths[i].rotation.x - this.oriRotation.x); dolly.rotation.y = this.oriRotation.y + this.pos * (paths[i].rotation.y - this.oriRotation.y); dolly.rotation.z = this.oriRotation.z + this.pos * (paths[i].rotation.z - this.oriRotation.z); } }); tween.onComplete(function(){ paths[i].fn && paths[i].fn(); var fn = tweens.shift(); fn && fn.start(); }); tweens.push(tween); })(i); } tweens.shift().start(); }
調用:
lib.cameraTracker([ {'pos': { x: -45,y: 5, z: -38},'rotation': {x: 0, y: -1.8, z: 0}, 'easing': TWEEN.Easing.Cubic.Out,'duration':4000} ]);
四、自適應長度文字提示
根據文字長度生成canvas做爲貼圖到Sprite對象。
hint = function(text, type, posY, fadeTime){ var chinense = text.replace(/[u4E00-u9FA5]/g, ''); var dbc = chinense.length; var sbc = text.length - dbc; var length = dbc * 2 + sbc; var fontsize = 40; var textWidth = fontsize* length / 2; posY = posY || 0.3; type = type || 1; fadeTime = fadeTime === window.undefined ? 500 : fadeTime; if(text == 'sucess' || text == 'fail') { text = ' '; } var canvas = document.createElement("canvas"); var width = 1024, height = 512; canvas.width = width; canvas.height = height; var context = canvas.getContext('2d'); var imageObj = document.querySelector('#img-hint-' + type); context.drawImage(imageObj, width/2 - imageObj.width/2, height/2 - imageObj.height/2); context.font = 'Bold '+ fontsize +'px simhei'; context.fillStyle = "rgba(255,255,255,1)"; context.fillText(text, width/2-textWidth/2, height/2+15); var texture = new THREE.Texture(canvas); texture.needsUpdate = true; var mesh; var material = new THREE.SpriteMaterial({ map: texture, transparent: true, opacity: 0 }); mesh = new THREE.Sprite(material); mesh.scale.set(width/400, height/400, 1); mesh.position.set(0, posY, -3); camera.add(mesh); var tweenIn = new TWEEN.Tween({pos: 0}).to({pos: 1}, fadeTime); tweenIn.onUpdate(function(){ material.opacity = this.pos; }); if(fadeTime === 0) { material.opacity = 1; } else { tweenIn.start(); } var tweenOut = new TWEEN.Tween({pos: 1}).to({pos: 0}, fadeTime); tweenOut.onUpdate(function(){ material.opacity = this.pos; }); tweenOut.onComplete(function(){ camera.remove(mesh); }); tweenOut.fadeOut = tweenOut.start; tweenOut.remove = function(){ camera.remove(mesh); } return tweenOut; };
五、unity地形導出
5.一、首先將unity地形導出爲obj
5.二、而後導入3dmax,使用ThreeJSExporter.ms導出爲js格式。
六、3dmax動畫導出問題
6.一、動畫導出錯誤
一般是對象爲可編輯多邊形,須要轉換成網格對象。
操做步驟:
6.1.一、選擇對象,右鍵轉換爲可編輯網絡;
6.1.二、選擇蒙皮修改器,從新蒙皮;
6.1.三、點擊蒙皮修改器下的骨骼 > 添加,添加原有的骨骼。
6.二、動畫導出錯亂
很容易讓人覺得是權重出問題了,但就我本身多個項目動畫導出的經驗來看,大部分出如今骨骼添加上。在3dmax及unity中,不添加根節點每每不影響動畫執行,但導出到three.js,須要添加根節點。若是問題還存在,則仔細觀察是哪一個骨骼引發的,多餘骨骼或缺乏骨骼均可能引發動畫錯亂。
5、完整的源代碼及相應組件
點擊下載main.js - 完整的源代碼tween.min.js - 動畫類OrbitControls.js - 視圖控制器,旋轉、移動、縮放場景,方便調試audio.min.js - motion音頻組件,解決自動播放音頻問題其他vr相關組件上文已有介紹