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

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

首先,作為一個基本的前端工程來說,我們需要讓代碼「工程化」,不僅要提供編譯構建、壓縮打包功能,還要讓每個頁面模塊化;

延伸到WebVR工程,我們也需要考慮就必須考慮「多頁面」模塊化,即提供多個場景模塊化開發,因為一個完整的WebVR App不僅僅只有一個場景。這裡可以參考google的WebVR多場景示例:vr.chromeexperiments.com

webvr多場景應用

多場景開發,最簡單的方式就是,一個場景對應一份html、css、js,多個頁面需要多個html,每次頁面跳轉需要重新進行VR渲染進行初始化。

實際上我們在多場景中,場景初始化只需要執行一次(比如,創建一個場景->創建相機->創建渲染器),我們只需要一個index.html作為入口頁面,將VR場景初始化、創建、回收、切換封裝成公用組件。

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

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

項目地址:github.com/YoneChen/web

Demo:https://YoneChen.github.io/webvr-webpack2-boilerplate/dist/

相關技術棧:three.jswebpack2es6/7

想詳細了解WebVR開發步驟,也歡迎參考我的文章《VR大潮來襲——前端開發能做些什麼》

實現功能

  • VR多場景模塊化開發
  • 支持VR場景創建、回收、切換
  • 項目自動化構建與壓縮打包

WebVR相關庫

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

主要目錄結構

webpack|-- webpack.config.js # 公共配置|-- webpack.dev.js # 開發配置|-- webpack.prod.js # 生產配置src # 項目源碼|-- views # WebVR場景目錄 | |-- page1.js| |-- page2.js |-- core # 核心目錄,包括webvr封裝類和polyfill| |-- VRCore.js| |-- VRPage.js| |-- vendor.js|-- assets # 素材目錄,包括3d模型、紋理、音頻等| |-- audio | |-- model| |-- texture|-- index.js # WebVR啟動頁|-- 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腳本。

首先是index.js入口,以配置場景的路由跳轉並傳入欲渲染的dom。

// src/index.jsconst 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"));

單個場景的頁面實例:

// src/views/page1.js// 繼承VRPage父類,開發每一個場景import VRPage from "core/js/VRPage";class Index extends VRPage { assets() { return { TEXTURE_SKYBOX: "texture/360bg.jpg" } } start() { // 啟動渲染前,創建添加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() { // 資源載入後鉤子函數 console.log(`page has been loaded.`); } update(delta) { // 動畫渲染鉤子函數 // animate }}export default Index;

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

  1. 每一個場景都可以進行獨立開發而互不影響;
  2. 一旦VR環境初始化之後,不需要在每次場景跳轉切換時重新初始化一遍。

WebVR多場景運行機制

VRCore.js作為公用模塊管理整個webvr應用的所有子場景,包括場景初始化、VR相機渲染、場景切換、場景回收等靜態函數。

VRPage.js作為每個場景的工廠類,支持不同3d頁面(場景)之間的代碼獨立。

每一個VR頁面的生命周期都是:創建物體->載入模型->啟動渲染的過程,因此,需要創建一個基類,來實現每一個VR場景實例的生命周期。

//common/VRPage.jsimport * 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. 新建場景
  • 創建VR相機
  • 載入場景腳本與資源
  • 開啟動畫渲染

VR環境初始化

function init(routers, container, fov, far) { createScene(...Array.prototype.slice.call(arguments,1)); Router.createRouter(routers); // 創建路由管理器}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基本開發步驟可以參考。

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

開啟動畫渲染

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

這裡動畫渲染主要封裝了three.js的renderer.animate()方法,入參作傳入一個callback回調方法,這個方法會在動畫渲染的每一幀中執行。

WebVR場景切換

主要包括四個步驟

  1. 暫停渲染
  2. 清空當前場景物體
  3. 請求並載入目標場景腳本與資源
  4. 重啟渲染

暫停動畫渲染

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

回收當前場景

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

按需載入

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

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

最終將清空當前場景與請求載入目標場景功能封裝為forward跳轉方法,就可以在頁面里直接調用了。

// src/core/VRCore.jsfunction 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 監聽事件 WebVR.Gazer.on(button, "gazeEnter",target => { // gazeIn trigger WebVR.forward("page2.js"); }); WebVR.Scene.add(box); }}export default Page1;// src/views/page2.jsclass Page2 extends VRPage {...}export default Page2;

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

VR單頁面路由管理

除了按需載入,考慮到是單頁面應用,我們還需對頁面的history堆棧進行管理,在實際的代碼中,頁面跳轉和按需載入被封裝成Router對象,管理頁面路由跳轉。

// src/core/VRCore.jsconst Router = { // 路由管理器初始化 createRouter(routes=[{"":"index.js"}]) { this.routeObj = {}; routes.forEach(route => { Object.defineProperty(this.routeObj,route.route,{ value:route.path }); }); this._proxyRouter(); this._historyProxy(); }, // 跳轉公用方法 forward(routeName,newtarget = true) { cleanPage(); const fileName = this._getFileName(routeName); if (newtarget) history.pushState({ routeName, fileName }, 0, routeName); this.fetchFile(fileName); }, // 當在地址欄輸入url,請求url路由對應的場景文件 _proxyRouter() { const routeName = this._getCurrentRouteName(); const fileName = this._getFileName(routeName); history.replaceState({ routeName, fileName }, 0, this._getCurrentRouteName()); this.fetchFile(fileName); }, // 監聽history堆棧變化,跳轉至對應場景 _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工程已經完成了一半,接下來,我們使用Webpack2來構建我們的工程。

Webpack配置

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

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首個場景src/page/index.js作為項目打包入口,同時將page目錄下的文件也作為單獨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作為公用模塊輸出,以省去在每個腳本import THREE from "three"的重複工作,同時將管理所有場景的核心模塊VRCore.js作為全局公用模塊輸出。

使用HtmlWebpackPlugin將公用的html打包到dist目錄下。

polyfill配置

最後是polyfill配置,我們需要引入webvr-polyfill來支持webvr API,作為一個頁面獨立腳本。

// core/vendor.jsimport "webvr-polyfill";

小結

以上WebVR工程已經基本搭建完畢,其重點是如下:

  • 根據場景設計了VR頁面實例的渲染周期
  • WebVR單頁面的路由管理和腳本動態請求

最後,歡迎關注專欄《WebVR技術莊園》,不定期更新,謝謝!


推薦閱讀:

【源碼拾遺】axios —— 極簡封裝的藝術
QQ音樂播放器簡易開發
【連載】Web應用到底是如何工作的?
為什麼要禁止跨域的 Ajax 請求?
開了N個知乎窗口,標題都有(1 條消息),點開其中一個窗口的消息提示後,(1 條消息)消失,緊接著其他所有標題都會陸續更新,什麼技術?

TAG:前端开发 | vr | JavaScript |