美文网首页iOS 功能类WebVRVR开发专辑
VR进化论|教你搭建通用的WebVR工程

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

作者: YoneChen | 来源:发表于2017-05-14 15:08 被阅读932次

    本文旨在介绍如何搭建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技术庄园》,不定期更新,谢谢!

    相关文章

      网友评论

      本文标题:VR进化论|教你搭建通用的WebVR工程

      本文链接:https://www.haomeiwen.com/subject/yiepottx.html