VR進化論|教你搭建通用的WebVR工程

本文旨在介紹如何搭建WebVR工程以支持多場景開發。css


首先,做爲一個基本的前端工程來講,咱們須要讓代碼「工程化」,不只要提供編譯構建、壓縮打包功能,還要讓每一個頁面模塊化;
延伸到WebVR工程,咱們也須要考慮就必須考慮「多頁面」模塊化,即提供多個場景模塊化開發,由於一個完整的WebVR App不單單隻有一個場景。這裏能夠參考google的WebVR多場景示例:https://vr.chromeexperiments....html

webvr多場景應用

多場景開發,最簡單的方式就是,一個場景對應一份html、css、js,多個頁面須要多個html,每次頁面跳轉須要從新進行VR渲染進行初始化。
實際上咱們在多場景中,場景初始化只須要執行一次(好比,建立一個場景->建立相機->建立渲染器),咱們只須要一個index.html做爲入口頁面,將VR場景初始化、建立、回收、切換封裝成公用組件。前端

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

在首次進入場景時進行初始化,在須要場景切換時進行場景回收和按需加載,這樣一來,用戶切換場景時,不用把時間浪費在等待html和初始化場景上。基於以上思路,本人總結的一套WebVR工程搭建方案,供各位參考。node

項目地址:https://github.com/YorkChan94...
Demo:https://yorkchan94.github.io/...
相關技術棧:three.jswebpack2es6/7
想詳細瞭解WebVR開發步驟,也歡迎參考個人文章《VR大潮來襲——前端開發能作些什麼》webpack

實現功能

  • VR多場景模塊化開發git

  • 支持VR場景建立、回收、切換es6

  • 項目自動化構建與壓縮打包github

  • 支持es7/6web

WebVR相關庫

  • three.jschrome

  • vrcontrols.js

  • vreffect.js

  • webvr-manager.js

  • webvr-polyfill.js

  • three-onevent.js

主要目錄結構

webpack
|-- webpack.config.js       # 公共配置
|-- webpack.dev.js          # 開發配置
|-- webpack.prod.js         # 生產配置
src                         # 項目源碼
|-- page                    # WebVR場景目錄       
|   |-- index.js            # WebVR入口場景              
|   |-- page1.js
|   |-- page2.js                                            
|-- common                  # 公共目錄,包括webvr封裝類和polyfill
|   |-- VRCore.js
|   |-- VRPage.js
|   |-- vendor.js
|-- lib                     # vr三方插件,包括相機控制器和分屏器
|   |-- vrcontrol.js 
|   |-- vreffect.js 
|-- assets                  # 素材目錄,包括3d模型、紋理、音頻等
|   |-- audio                      
|   |-- model
|   |-- texture
|-- index.html              # WebVR公用頁面
package.json                        
READNE.md

咱們先來看看index.html,其實整個body就只有一個dom,用來append咱們的canvas,畢竟因此場景都在canvas裏運行。

<!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,咱們但願這樣開發WebVR應用,即一個場景對應一個js腳本,形如:

// 繼承VRPage父類,開發每個場景
import VRPage from 'common/js/VRPage';

class Page1 extends VRPage {
    start() { // 啓動渲染以前,建立場景3d模型
        let geometry = new THREE.CubeGeometry(5,5,5);
        let material = new THREE.MeshBasicMaterial( { color:0x00aadd} );
        this.box = new THREE.Mesh(geometry,material);
        this.box.position.set(3,-2,-3);
        WebVR.Scene.add(this.box);
    }
    loaded() { // 場景資源加載完畢,可執行音頻播放等。
    }
    update(delta) { // 開啓渲染以後,執行模型動畫
        this.box.rotation.y += 0.05;
    }
}
export default (() => {
    return new Page1();
})();

這裏參照了相似Unity3d和React的開發模式,在start方法裏建立3d模型,在update方法裏處理3d動畫,這樣的好處在於:

  1. 每個場景均可以進行獨立開發而互不影響;

  2. 一旦VR環境初始化以後,不須要在每次場景跳轉切換時從新初始化一遍。

WebVR多場景運行機制

VRCore.js做爲公用模塊管理整個webvr應用的全部子場景,包括場景初始化、VR相機渲染、場景切換、場景回收等靜態函數。
VRPage.js做爲每一個場景的工廠類,支持不一樣3d頁面(場景)之間的代碼獨立。
每個VR頁面的生命週期都是:建立物體->加載模型->啓動渲染的過程,所以,須要建立一個基類,來實現每個VR場景實例的生命週期。

//common/VRPage.js
import * as WebVR from 'VRCore.js' //管理全部場景的公用模塊
// VR場景工廠
export default class VRPage {
    constructor(options={}) {
        // 建立場景,若是場景已初始化
        WebVR.createScene(options);
        this.start();
        this.loadPage();
    }
    loadPage() {
        THREE.DefaultLoadingManager.onLoad = () => {
            // 模型加載完畢,即開啓渲染
            WebVR.renderStart(this.update);
            this.loaded(); 
        }
    }
    start() { 
         // 實例的start方法將在啓動渲染以前,場景相機初始化後執行。
    }
    loaded() {
        // 實例的loaded方法將在場景資源加載後執行。
    }
    update(delta) { 
        // 實例的update方法將在渲染器每一次渲染時執行。
    }
}

這裏使用THREE.DefaultLoadingManager.onLoad方法監聽場景是否加載完畢,一旦加載完畢,便啓動渲染。

WebVR場景首次渲染

主要包括四個步驟

  1. 新建場景

  2. 建立VR相機

  3. 加載場景腳本與資源

  4. 開啓動畫渲染

VR環境初始化
function createScene({domContainer=document.body,fov=70,far=4000}) {
    // 建立場景
    Scene = new THREE.Scene();
    // 建立相機
    Camera = new THREE.PerspectiveCamera(fov,window.innerWidth/window.innerHeight,0.1,far);
    Camera.position.set( 0, 0, 0 );
    Scene.add(Camera);
    // 建立渲染器
    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開發三部曲,建立場景、相機、渲染器,接着調用initVR函數來完成VR場景分屏和陀螺儀控制,WebVR基本開發步驟能夠參考。

function initVR() {
    // 初始化VR分屏器和控制器
    Effect = new THREE.VREffect(Renderer);
    Controls = new THREE.VRControls(Camera);
    // 初始化VR管理器
    Manager = new WebVRManager(Renderer, Effect);
    window.addEventListener( 'resize', e => {
        // 調整渲染器和相機以適應窗口拉伸時寬高變更
        Camera.aspect = window.innerWidth / window.innerHeight;
        Camera.updateProjectionMatrix();
        Effect.setSize(window.innerWidth, window.innerHeight);
    }, false );
}
開啓動畫渲染
// VRCore.js
function renderStart(callback) {
    // 設置loopID變量記錄每一幀ID
    loopID = 0;
    const loop = () => {
        if(loopID === -1) return;
        loopID = requestAnimationFrame(loop);
        callback();
        Controls.update();
        Manager.render(Scene, Camera);
    };
    loop();
}

這裏傳入參數動畫渲染作了三件事,使用loopID做爲整個VR應用的全局變量,記錄每一幀動畫的更新;更新相機控制器和VR渲染器,

WebVR場景切換

主要包括四個步驟

  1. 暫停渲染

  2. 清空當前場景物體

  3. 請求並加載目標場景腳本與資源

  4. 重啓渲染

暫停動畫渲染
function renderStop() {
    if (loopID !== -1) {
        window.cancelAnimationFrame(loopID);
        loopID = -1;
    }
}
回收當前場景
function clearScene() {
    for(let i = Scene.children.length - 1; i >= 0; i-- ) {
        Scene.remove(Scene.children[i]);
    }
}
按需加載

切換到下一場景,咱們須要請求對應的場景腳本,這裏使用webpack2的import函數進行代碼分離,固然你也可使用require.ensure(filename => {require(filename)})方法。

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

最終將清空當前場景與請求加載目標場景功能封裝爲forward跳轉方法,就能夠在頁面裏直接調用了。

// common/VRCore.js
function forward(fileName) {
    renderStop();
    clearScene();
    import(`page/${fileName}.js`);
}
// page/index.js
...
class Index extends VRPage {
    start() {
        let geometry = new THREE.CubeGeometry(5,5,5);
        let material = new THREE.MeshBasicMaterial({ 
            color: 0x00aadd
        });
        this.box = new THREE.Mesh(geometry,material);
        this.box.position.set(3,-2,-3);
        // add gaze eventLisenter
        this.box.on('gaze',mesh => { // gazeIn trigger
            WebVR.forward('page2.js');
        });
        WebVR.Scene.add(box);
    }
}
...
// page2.js
class page2 extends VRPage {
    start() {
        this.addPanorama(1000, ASSET_TEXTURE_SKYBOX);
    }
    addPanorama(radius,path) {
        // create panorama
        let geometry = new THREE.SphereGeometry(radius,50,50);
        let material = new THREE.MeshBasicMaterial( { map: new THREE.TextureLoader().load(path),side:THREE.BackSide } );
        let panorama = new THREE.Mesh(geometry,material);
        WebVR.Scene.add(panorama);
        return panorama;
    }
}
export default (() => {
    return new page2();
})();

咱們在場景裏建立一個立方體,當凝視到該物體時,執行forward方法跳轉至page2場景。

至此,咱們的WebVR工程已經完成了一半,接下來,咱們使用Webpack2來構建咱們的工程。

webpack配置

開發環境和生產環境下webpack配置略有不一樣,這裏主要給出webpack的基本配置,具體可參考項目地址。

const path = require('path');
const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ProvidePlugin = require('webpack/lib/ProvidePlugin');

module.exports = {

  entry: {
    'vendor': './src/common/js/vendor.js',
    'app': './src/page/index.js'
  },

  output: {
    path: path.resolve(__dirname, '../dist/'),
    filename: '[name].js',
    sourceMapFilename: '[name].map',
    chunkFilename: '[id]-chunk.js',
    publicPath: '/'
  },

這裏咱們將webvr首個場景src/page/index.js做爲項目打包入口,同時將page目錄下的文件也做爲單獨chunk,配合按需加載來支持場景切換。

module: {
    rules: [
      {
        test: /\.js/,
        exclude: /node_modules/,
        use: [
          { loader:'babel-loader',options: { 
            presets: ["latest",["es2015", {"modules": false}]] 
          }
        ]
      },
      {
        test: /\.css/,
        use: ['style-loader','css-loader']
      },
      {
        test: /\.(jpg|png|mp4|wav|ogg|obj|mtl|dae)$/,
        loader: 'file-loader'
      }
    ]

  },

這裏引入file-loader,這樣就能在場景裏直接import須要用到的素材,以下。

//page/page2.js
import ASSET_TEXTURE_SKYBOX from 'assets/texture/360_page2.jpg';

webpack相關的plugin配置以下

plugins: [
    new CommonsChunkPlugin({
      name: ['app', 'vendor'],
      minChunks: Infinity
    }),
    new ProvidePlugin({
      'THREE': 'three',
      'WebVR': path.resolve(__dirname,'../src/common/js/VRCore.js')
    }),
    new HtmlWebpackPlugin({
      inject: true,
      template: path.resolve(__dirname, '../src/index.html'),
      favicon: path.resolve(__dirname, '../src/favicon.ico')
    })
  ]

};

使用ProvidePluginthree.js做爲公用模塊輸出,以省去在每一個腳本import THREE from 'three'的重複工做,同時將管理全部場景的核心模塊VRCore.js做爲全局公用模塊輸出。
使用HtmlWebpackPlugin將公用的html打包到dist目錄下。

polyfill配置

最後是polyfill配置,咱們須要引入webvr-polyfill和babel-polyfill來分別支持webvr API和ES6 API,並做爲一個頁面獨立腳本。

// common/vendor.js
import 'babel-polyfill';
import 'webvr-polyfill';

以上WebVR工程已經基本搭建完畢,歡迎各位提出寶貴意見,後續咱們將探索daydream和Oculus在webvr上的開發模式,敬請期待。

最後,獻上前幾天在google開發者網站上看到的:預測將來,不如創造將來。

相關文章
相關標籤/搜索