VR進(jìn)化論|教你搭建通用的WebVR工程


本文旨在介紹如何搭建WebVR單頁面工程以支持多場景開發(fā)。


首先虹曙,作為一個(gè)基本的前端工程來說,我們需要讓代碼“工程化”番舆,不僅要提供編譯構(gòu)建酝碳、壓縮打包功能,還要讓每個(gè)頁面模塊化恨狈;
延伸到WebVR工程击敌,我們也需要考慮就必須考慮“多頁面”模塊化,即提供多個(gè)場景模塊化開發(fā)拴事,因?yàn)橐粋€(gè)完整的WebVR App不僅僅只有一個(gè)場景。這里可以參考google的WebVR多場景示例:https://vr.chromeexperiments.com/

webvr多場景應(yīng)用

多場景開發(fā)圣蝎,最簡單的方式就是刃宵,一個(gè)場景對(duì)應(yīng)一份html、css徘公、js牲证,多個(gè)頁面需要多個(gè)html,每次頁面跳轉(zhuǎn)需要重新進(jìn)行VR渲染進(jìn)行初始化关面。
實(shí)際上我們?cè)诙鄨鼍爸刑古郏瑘鼍俺跏蓟恍枰獔?zhí)行一次(比如,創(chuàng)建一個(gè)場景->創(chuàng)建相機(jī)->創(chuàng)建渲染器)等太,我們只需要一個(gè)index.html作為入口頁面捂齐,將VR場景初始化、創(chuàng)建缩抡、回收奠宜、切換封裝成公用組件。

WebVR場景切換,用戶的耐心是有限的

在首次進(jìn)入場景時(shí)進(jìn)行初始化压真,在需要場景切換時(shí)進(jìn)行場景回收和按需加載娩嚼,這樣一來,用戶切換場景時(shí)滴肿,不用把時(shí)間浪費(fèi)在等待html和初始化場景上岳悟。基于以上思路泼差,本人總結(jié)的一套WebVR工程搭建方案贵少,供各位參考。

項(xiàng)目地址:https://github.com/YoneChen/webvr-webpack2-boilerplate
Demo:https://YoneChen.github.io/webvr-webpack2-boilerplate/dist/
相關(guān)技術(shù)棧:three.js拴驮、webpack2春瞬、es6/7
想詳細(xì)了解WebVR開發(fā)步驟,也歡迎參考我的文章《VR大潮來襲——前端開發(fā)能做些什么》

實(shí)現(xiàn)功能

  • VR多場景模塊化開發(fā)
  • 支持VR場景創(chuàng)建套啤、回收宽气、切換
  • 項(xiàng)目自動(dòng)化構(gòu)建與壓縮打包

WebVR相關(guān)庫

  • three.js
  • tween.js
  • webvr-polyfill.js

主要目錄結(jié)構(gòu)

webpack
|-- webpack.config.js       # 公共配置
|-- webpack.dev.js          # 開發(fā)配置
|-- webpack.prod.js         # 生產(chǎn)配置
src                         # 項(xiàng)目源碼
|-- views                   # WebVR場景目錄                
|   |-- page1.js
|   |-- page2.js                                            
|-- core                  # 核心目錄,包括webvr封裝類和polyfill
|   |-- VRCore.js
|   |-- VRPage.js
|   |-- vendor.js
|-- assets                  # 素材目錄潜沦,包括3d模型萄涯、紋理、音頻等
|   |-- audio                      
|   |-- model
|   |-- texture
|-- index.js              # WebVR啟動(dòng)頁
|-- index.html              # WebVR公用頁面
package.json                        
READNE.md

我們先來看看index.html唆鸡,其實(shí)整個(gè)body就只有一個(gè)dom涝影,用來append我們的canvas,畢竟所以場景都在canvas里運(yùn)行争占。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0, shrink-to-fit=no">
    <title>webVR-INDEX</title>
</head>
<body>
  <section class="webvr-container"></section>
</body>
</html>

有了公用html燃逻,我們希望這樣開發(fā)WebVR應(yīng)用,配置一個(gè)入口路由列表臂痕,一個(gè)場景對(duì)應(yīng)一個(gè)js腳本伯襟。
首先是index.js入口,以配置場景的路由跳轉(zhuǎn)并傳入欲渲染的dom握童。

// src/index.js
const routes = [
    {
        route: '', // e.g http://127.0.1:9000/
        path: 'page1.js'
    },
    {
        route: '2', // e.g http://127.0.1:9000/2
        path: 'page2.js'
    }
];
WebVR.init(routes, document.querySelector('.webvr-container'));

單個(gè)場景的頁面實(shí)例:

// src/views/page1.js
// 繼承VRPage父類姆怪,開發(fā)每一個(gè)場景
import VRPage from 'core/js/VRPage';

class Index extends VRPage {
  assets() {
    return {
      TEXTURE_SKYBOX: 'texture/360bg.jpg'
    }
  }
  start() {
     // 啟動(dòng)渲染前,創(chuàng)建添加3d模型澡绩,比如天空稽揭、地面、燈光肥卡、背景音等
    const { TEXTURE_SKYBOX } = this.assets;
    const geometry = new THREE.SphereGeometry(radius,50,50);
    const material = new THREE.MeshBasicMaterial( { map: new THREE.TextureLoader().load(TEXTURE_SKYBOX),side:THREE.BackSide } );
    const panorama = new THREE.Mesh(geometry,material);
    WebVR.Scene.add(panorama);
  }
  loaded() { // 資源加載后鉤子函數(shù)
    console.log(`page has been loaded.`);
  }
  update(delta) { // 動(dòng)畫渲染鉤子函數(shù)
    // animate
  }
}
export default Index;

這里參照了類似Unity3d和React的開發(fā)模式溪掀,在start方法里創(chuàng)建3d模型,在update方法里處理3d動(dòng)畫召调,這樣的好處在于:

  1. 每一個(gè)場景都可以進(jìn)行獨(dú)立開發(fā)而互不影響膨桥;
  2. 一旦VR環(huán)境初始化之后蛮浑,不需要在每次場景跳轉(zhuǎn)切換時(shí)重新初始化一遍。
WebVR多場景運(yùn)行機(jī)制

VRCore.js作為公用模塊管理整個(gè)webvr應(yīng)用的所有子場景只嚣,包括場景初始化沮稚、VR相機(jī)渲染、場景切換册舞、場景回收等靜態(tài)函數(shù)蕴掏。
VRPage.js作為每個(gè)場景的工廠類,支持不同3d頁面(場景)之間的代碼獨(dú)立调鲸。
每一個(gè)VR頁面的生命周期都是:創(chuàng)建物體->加載模型->啟動(dòng)渲染的過程盛杰,因此,需要?jiǎng)?chuàng)建一個(gè)基類藐石,來實(shí)現(xiàn)每一個(gè)VR場景實(shí)例的生命周期即供。

//common/VRPage.js
import * as WebVR from 'VRCore.js' //管理所有場景的公用模塊
// VR場景工廠
export default class VRPage {
    constructor(options={}) {
        // 創(chuàng)建場景,如果場景已初始化
        WebVR.createScene(options);
        this.start();
        this.loadPage();
    }
    loadPage() {
        THREE.DefaultLoadingManager.onLoad = () => {
            // 模型加載完畢于微,即開啟渲染
            WebVR.renderStart(this.update);
            this.loaded(); 
        }
    }
    start() { 
         // 實(shí)例的start方法將在啟動(dòng)渲染之前逗嫡,場景相機(jī)初始化后執(zhí)行。
    }
    loaded() {
        // 實(shí)例的loaded方法將在場景資源加載后執(zhí)行株依。
    }
    update(delta) { 
        // 實(shí)例的update方法將在渲染器每一次渲染時(shí)執(zhí)行驱证。
    }
}

這里使用THREE.DefaultLoadingManager.onLoad方法監(jiān)聽場景是否加載完畢,一旦加載完畢恋腕,便啟動(dòng)渲染抹锄。

WebVR場景首次渲染

主要包括四個(gè)步驟

  1. 新建場景
  • 創(chuàng)建VR相機(jī)
  • 加載場景腳本與資源
  • 開啟動(dòng)畫渲染

VR環(huán)境初始化

function init(routers, container, fov, far) {
  createScene(...Array.prototype.slice.call(arguments,1));
  Router.createRouter(routers); // 創(chuàng)建路由管理器
}
function createScene({domContainer=document.body,fov=70,far=4000}) {
    // 創(chuàng)建場景
    Scene = new THREE.Scene();
    // 創(chuàng)建相機(jī)
    Camera = new THREE.PerspectiveCamera(fov,window.innerWidth/window.innerHeight,0.1,far);
    Camera.position.set( 0, 0, 0 );
    Scene.add(Camera);
    // 創(chuàng)建渲染器
    Renderer = new THREE.WebGLRenderer({ antialias: true } );
    Renderer.setSize(window.innerWidth,window.innerHeight);
    Renderer.shadowMapEnabled = true;
    Renderer.setPixelRatio(window.devicePixelRatio);
    domContainer.appendChild(Renderer.domElement);
    initVR();
    resize();
}

首先是three.js開發(fā)三部曲,創(chuàng)建場景荠藤、相機(jī)伙单、渲染器,接著調(diào)用initVR函數(shù)來完成VR場景分屏和陀螺儀控制哈肖,WebVR基本開發(fā)步驟可以參考车份。

let Display;
function initVR() {
  // 獲取VR設(shè)備,通知渲染器啟動(dòng)VR渲染模式
  Renderer.vr.enabled = true;
  // 獲取VR頭顯實(shí)例
  navigator.getVRDisplays().then( display => {
    Display = display[0];
    Renderer.vr.setDevice(Display);
    // 初始化控制VR渲染模式的控制按鈕
    VRButton.init(Renderer.domElement.parentNode,Display,Renderer);
     }).catch(err => console.warn(err));
}

開啟動(dòng)畫渲染

// VRCore.js
function renderStart(callback) {
  Renderer.animate(function() {
    callback();
    TWEEN.update();
    Renderer.render(Scene, Camera);
  });
}

這里動(dòng)畫渲染主要封裝了three.js的renderer.animate()方法牡彻,入?yún)⒆鱾魅胍粋€(gè)callback回調(diào)方法,這個(gè)方法會(huì)在動(dòng)畫渲染的每一幀中執(zhí)行出爹。

WebVR場景切換

主要包括四個(gè)步驟

  1. 暫停渲染
  • 清空當(dāng)前場景物體
  • 請(qǐng)求并加載目標(biāo)場景腳本與資源
  • 重啟渲染

暫停動(dòng)畫渲染

function renderStop() {
  Renderer.dispose(); // 暫停渲染器渲染
  TWEEN.removeAll(); // 移除所有tween動(dòng)畫
}

回收當(dāng)前場景

function clearScene() {
  for(let i = Scene.children.length - 1; i >= 0; i-- ) {
  if (Scene.children[i].type === 'PerspectiveCamera') continue; // 保留相機(jī)
    Scene.remove(Scene.children[i]); // 移除當(dāng)前場景中的物體
  }
  Scene.fog = null; // 清除場景霧
}

按需加載

切換到下一場景庄吼,我們需要請(qǐng)求對(duì)應(yīng)的場景腳本,這里使用webpack2的import函數(shù)進(jìn)行代碼分離严就,當(dāng)然你也可以使用require.ensure(filename => {require(filename)})方法总寻。

import(`views/${fileName}.js`);

最終將清空當(dāng)前場景與請(qǐng)求加載目標(biāo)場景功能封裝為forward跳轉(zhuǎn)方法,就可以在頁面里直接調(diào)用了梢为。

// src/core/VRCore.js
function forward(fileName) {
  renderStop();
  clearScene();
  import(`views/${fileName}.js`);
}
// src/views/page1.js
...
class Page1 extends VRPage {
  start() {
    const geometry = new THREE.CubeGeometry(5,5,5);
    const material = new THREE.MeshBasicMaterial({ color: 0x00aadd });
    const button = new THREE.Mesh(geometry,material);
    button.position.set(3,-2,-3);
    // 添加 gaze 監(jiān)聽事件
    WebVR.Gazer.on(button, 'gazeEnter',target => { // gazeIn trigger
      WebVR.forward('page2.js');
    });
    WebVR.Scene.add(box);
  }
}
export default Page1;

// src/views/page2.js
class Page2 extends VRPage {
...
}
export default Page2;

我們?cè)?code>page1場景里創(chuàng)建一個(gè)立方體渐行,當(dāng)凝視到該物體時(shí)轰坊,執(zhí)行forward方法跳轉(zhuǎn)至page2場景。

VR單頁面路由管理

除了按需加載祟印,考慮到是單頁面應(yīng)用肴沫,我們還需對(duì)頁面的history堆棧進(jìn)行管理,在實(shí)際的代碼中蕴忆,頁面跳轉(zhuǎn)和按需加載被封裝成Router對(duì)象颤芬,管理頁面路由跳轉(zhuǎn)。

// src/core/VRCore.js
const Router = {
  // 路由管理器初始化
  createRouter(routes=[{'':'index.js'}]) { 
    this.routeObj = {};
    routes.forEach(route => {
      Object.defineProperty(this.routeObj,route.route,{ value:route.path }); 
    });
    this._proxyRouter();
    this._historyProxy();
  },
  // 跳轉(zhuǎn)公用方法
  forward(routeName,newtarget = true) {
    cleanPage();
    const fileName = this._getFileName(routeName);
    if (newtarget) history.pushState({ routeName, fileName }, 0, routeName);
    this.fetchFile(fileName);
  },
  // 當(dāng)在地址欄輸入url套鹅,請(qǐng)求url路由對(duì)應(yīng)的場景文件
  _proxyRouter() {
    const routeName = this._getCurrentRouteName();
    const fileName = this._getFileName(routeName);
    history.replaceState({ routeName, fileName }, 0, this._getCurrentRouteName());
    this.fetchFile(fileName);
  },
  // 監(jiān)聽history堆棧變化站蝠,跳轉(zhuǎn)至對(duì)應(yīng)場景
  _historyProxy() {
    window.addEventListener('popstate',e => {
      const routeName = e.state.routeName;
      this.forward(routeName,false);
    },false);
  },
  _getCurrentRouteName() { return location.pathname.split('/').pop(); },
  _getFileName(routeName) { return this.routeObj[routeName] || ''; },
     ...
};
Router.fetchFile = function(fileName) {
  import(`views/${fileName}`).then(page => {
    new page.default();
  });
};

至此,我們的WebVR工程已經(jīng)完成了一半卓鹿,接下來菱魔,我們使用Webpack2來構(gòu)建我們的工程。

Webpack配置

開發(fā)環(huán)境和生產(chǎn)環(huán)境下webpack配置略有不同吟孙,這里主要給出webpack的基本配置澜倦,具體可參考項(xiàng)目地址。

const path = require('path');
const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const ProvidePlugin = require('webpack/lib/ProvidePlugin');
module.exports = {
  entry: {
    'vendor': './src/core/js/vendor.js',
    'app': './src/index.js'
  },
  output: {
    path: path.resolve(__dirname, '../dist/'),
    filename: '[name].js',
    sourceMapFilename: '[name].map',
    chunkFilename: '[id]-chunk.js',
    publicPath: '/'
  },

這里我們將webvr首個(gè)場景src/page/index.js作為項(xiàng)目打包入口拔疚,同時(shí)將page目錄下的文件也作為單獨(dú)chunk肥隆,配合按需加載來支持場景切換。

module: {
  rules: [{
      test: /\.js/,
      use: "babel-loader",
    },
    {
      test: /\.css/,
      use: ['style-loader','css-loader']
    },
    {
      test: /\.(glsl|vs|fs)$/,
      loader: 'shader-loader',
    },
  },
  plugins: [
    new CommonsChunkPlugin({
      name: ['app', 'vendor'],
      minChunks: Infinity
    }),
    new CopyWebpackPlugin([{ from: path.resolve(__dirname,'../src/assets') }]),
    new ProvidePlugin({
      'THREE': 'three'稚失,
      'WebVR': path.resolve(__dirname,'../src/core/VRCore.js')
    }),
    new HtmlWebpackPlugin({
      inject: true,
      template: path.resolve(__dirname, '../src/index.html'),
      favicon: path.resolve(__dirname, '../src/favicon.ico')
    })
  ]
};

使用ProvidePluginthree.js作為公用模塊輸出栋艳,以省去在每個(gè)腳本import THREE from 'three'的重復(fù)工作,同時(shí)將管理所有場景的核心模塊VRCore.js作為全局公用模塊輸出句各。
使用HtmlWebpackPlugin將公用的html打包到dist目錄下吸占。

polyfill配置

最后是polyfill配置,我們需要引入webvr-polyfill來支持webvr API凿宾,作為一個(gè)頁面獨(dú)立腳本矾屯。

// core/vendor.js
import 'webvr-polyfill';

小結(jié)

以上WebVR工程已經(jīng)基本搭建完畢,其重點(diǎn)是如下:

  • 根據(jù)場景設(shè)計(jì)了VR頁面實(shí)例的渲染周期
  • WebVR單頁面的路由管理和腳本動(dòng)態(tài)請(qǐng)求

最后初厚,歡迎關(guān)注專欄《WebVR技術(shù)莊園》件蚕,不定期更新,謝謝产禾!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末排作,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子亚情,更是在濱河造成了極大的恐慌妄痪,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件楞件,死亡現(xiàn)場離奇詭異衫生,居然都是意外死亡裳瘪,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門罪针,熙熙樓的掌柜王于貴愁眉苦臉地迎上來彭羹,“玉大人,你說我怎么就攤上這事站故〗耘拢” “怎么了?”我有些...
    開封第一講書人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵西篓,是天一觀的道長愈腾。 經(jīng)常有香客問我,道長岂津,這世上最難降的妖魔是什么虱黄? 我笑而不...
    開封第一講書人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮吮成,結(jié)果婚禮上橱乱,老公的妹妹穿的比我還像新娘。我一直安慰自己粱甫,他們只是感情好泳叠,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著茶宵,像睡著了一般危纫。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上乌庶,一...
    開封第一講書人閱讀 51,146評(píng)論 1 297
  • 那天种蝶,我揣著相機(jī)與錄音,去河邊找鬼瞒大。 笑死螃征,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的透敌。 我是一名探鬼主播盯滚,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼酗电!你這毒婦竟也來了淌山?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤顾瞻,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后德绿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體荷荤,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡退渗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蕴纳。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片会油。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖古毛,靈堂內(nèi)的尸體忽然破棺而出翻翩,到底是詐尸還是另有隱情,我是刑警寧澤稻薇,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布嫂冻,位于F島的核電站,受9級(jí)特大地震影響塞椎,放射性物質(zhì)發(fā)生泄漏桨仿。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一案狠、第九天 我趴在偏房一處隱蔽的房頂上張望服傍。 院中可真熱鬧,春花似錦骂铁、人聲如沸吹零。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽灿椅。三九已至,卻和暖如春名段,著一層夾襖步出監(jiān)牢的瞬間阱扬,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來泰國打工伸辟, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留麻惶,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓信夫,卻偏偏與公主長得像窃蹋,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子静稻,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,072評(píng)論 25 707
  • WebVR即web + VR的體驗(yàn)方式警没,我們可以戴著頭顯享受沉浸式的網(wǎng)頁,新的API標(biāo)準(zhǔn)讓我們可以使用js語言來開...
    YoneChen閱讀 11,291評(píng)論 16 66
  • react vr中文網(wǎng):www.vr-react.com react vr qq群:481244084 示例源碼 ...
    liu_520閱讀 3,657評(píng)論 4 6
  • 最近WebVR API 1.1已經(jīng)發(fā)布振湾,2.0草案也在擬定中杀迹,在我看來,WebVR走向大眾瀏覽器是早晚的事情了押搪,今...
    YoneChen閱讀 13,615評(píng)論 2 16
  • 從昨天晚上到今天跟咖啡撕逼的過程中树酪,我清楚的看到了我在親密關(guān)系里的模式浅碾。我在咖啡身上投射了很多東西,我希望給她無條...
    萌萌是大王閱讀 240評(píng)論 0 0