本文旨在介紹如何搭建WebVR單頁面工程以支持多場景開發(fā)。
首先虹曙,作為一個(gè)基本的前端工程來說,我們需要讓代碼“工程化”番舆,不僅要提供編譯構(gòu)建酝碳、壓縮打包功能,還要讓每個(gè)頁面模塊化恨狈;
延伸到WebVR工程击敌,我們也需要考慮就必須考慮“多頁面”模塊化,即提供多個(gè)場景模塊化開發(fā)拴事,因?yàn)橐粋€(gè)完整的WebVR App不僅僅只有一個(gè)場景。這里可以參考google的WebVR多場景示例:https://vr.chromeexperiments.com/
多場景開發(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)建缩抡、回收奠宜、切換封裝成公用組件。
在首次進(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)畫召调,這樣的好處在于:
- 每一個(gè)場景都可以進(jìn)行獨(dú)立開發(fā)而互不影響膨桥;
- 一旦VR環(huán)境初始化之后蛮浑,不需要在每次場景跳轉(zhuǎn)切換時(shí)重新初始化一遍。
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è)步驟
- 新建場景
- 創(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è)步驟
- 暫停渲染
- 清空當(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')
})
]
};
使用ProvidePlugin
將three.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ù)莊園》件蚕,不定期更新,謝謝产禾!