VR进化论|教你搭建通用的WebVR工程


本文旨在介绍如何搭建WebVR单页面工程以支持多场景开发。


首先,作为一个基本的前端工程来说,我们需要让代码“工程化”,不仅要提供编译构建、压缩打包功能,还要让每个页面模块化;
延伸到WebVR工程,我们也需要考虑就必须考虑“多页面”模块化,即提供多个场景模块化开发,因为一个完整的WebVR App不仅仅只有一个场景。这里可以参考google的WebVR多场景示例:https://vr.chromeexperiments.com/

webvr多场景应用

多场景开发,最简单的方式就是,一个场景对应一份html、css、js,多个页面需要多个html,每次页面跳转需要重新进行VR渲染进行初始化。
实际上我们在多场景中,场景初始化只需要执行一次(比如,创建一个场景->创建相机->创建渲染器),我们只需要一个index.html作为入口页面,将VR场景初始化、创建、回收、切换封装成公用组件。

WebVR场景切换,用户的耐心是有限的

在首次进入场景时进行初始化,在需要场景切换时进行场景回收和按需加载,这样一来,用户切换场景时,不用把时间浪费在等待html和初始化场景上。基于以上思路,本人总结的一套WebVR工程搭建方案,供各位参考。

项目地址:https://github.com/YoneChen/webvr-webpack2-boilerplate
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.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'));

单个场景的页面实例:

// 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.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. 新建场景
  • 创建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.js
function renderStart(callback) {
  Renderer.animate(function() {
    callback();
    TWEEN.update();
    Renderer.render(Scene, Camera);
  });
}

这里动画渲染主要封装了three.js的renderer.animate()方法,入参作传入一个callback回调方法,这个方法会在动画渲染的每一帧中执行。

WebVR场景切换

主要包括四个步骤

  1. 暂停渲染
  • 清空当前场景物体
  • 请求并加载目标场景脚本与资源
  • 重启渲染

暂停动画渲染

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.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 监听事件
    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;

我们在page1场景里创建一个立方体,当凝视到该物体时,执行forward方法跳转至page2场景。

VR单页面路由管理

除了按需加载,考虑到是单页面应用,我们还需对页面的history堆栈进行管理,在实际的代码中,页面跳转和按需加载被封装成Router对象,管理页面路由跳转。

// 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();
  },
  // 跳转公用方法
  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.js
import 'webvr-polyfill';

小结

以上WebVR工程已经基本搭建完毕,其重点是如下:

  • 根据场景设计了VR页面实例的渲染周期
  • WebVR单页面的路由管理和脚本动态请求

最后,欢迎关注专栏《WebVR技术庄园》,不定期更新,谢谢!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • WebVR即web + VR的体验方式,我们可以戴着头显享受沉浸式的网页,新的API标准让我们可以使用js语言来开...
    YoneChen阅读 11,207评论 16 66
  • react vr中文网:www.vr-react.com react vr qq群:481244084 示例源码 ...
    liu_520阅读 3,499评论 4 6
  • 最近WebVR API 1.1已经发布,2.0草案也在拟定中,在我看来,WebVR走向大众浏览器是早晚的事情了,今...
    YoneChen阅读 13,505评论 2 16
  • 从昨天晚上到今天跟咖啡撕逼的过程中,我清楚的看到了我在亲密关系里的模式。我在咖啡身上投射了很多东西,我希望给她无条...
    萌萌是大王阅读 228评论 0 0