├── README.md
├── audio
└── horn.wav
├── cardboard.html
├── common
└── vrbase.js
├── examples
├── 3d-audio.html
├── cardboard2.html
├── physics.html
└── voice.html
├── gearvr.html
├── index.html
├── texture
└── road.jpg
└── vendor
├── Projector.js
├── VRControls.js
├── VREffect.js
├── cannon.min.js
├── stats.min.js
├── three.min.js
├── tween.min.js
├── webvr-manager.js
└── webvr-polyfill.js
/README.md:
--------------------------------------------------------------------------------
1 | # WebVR-helloworld
2 | a webVR 'hello world' project base in three.js
3 |
4 | English language version will soon be released.
5 |
6 | 
7 | WebVR即web + VR的体验方式,我们可以戴着头显享受沉浸式的网页,新的API标准让我们可以使用js语言来开发。本文将介绍如何快速开发一个WebVR网页,在此之前,我们有必要了解WebVR的体验方式。
8 |
9 | ## WebVR体验模式
10 | ---
11 |
12 | 
13 | ***WebVR的体验方式可以分为VR模式和裸眼模式***
14 |
15 | ### VR模式
16 |
17 | ***1.Mobile VR***
18 |
19 | 如使用cardboard眼镜来体验手机浏览器的webVR网页,浏览器将根据水平陀螺仪的参数来获取用户的头部倾斜和转动的朝向,并告知页面需要渲染哪一个朝向的场景。
20 |
21 | ***2.PC VR***
22 |
23 | 通过佩戴Oculus Rift的分离式头显浏览连接在PC主机端的网页,现支持WebVR API的浏览器主要是火狐的 [Firefox Nightly](https://nightly.mozilla.org/)和设置VR enabled的谷歌chrome beta。
24 |
25 | ### 裸眼模式
26 |
27 | 除了VR模式下的体验方式,这里还考虑了裸眼下的体验浏览网页的方式,在PC端如果探测的用户选择进入VR模式,应让用户可以使用鼠标拖拽场景,而在智能手机上则应让用户可以使用touchmove或旋转倾斜手机的方式来改变场景视角。
28 | WebVR的概念大概就如此,这次我们将采用cardboard + mobile的方式来测试我们的WebVR场景,现在踏上我们的开发之旅。
29 |
30 | ## WebVR的开发环境配置
31 | 由于WebVR App需要运行VR设备上,而目前购买一台VR设备的成本不低,所以这里总结了一套开发环境下WebVR调试方案。
32 | 首先我们需要给WebVR静态页面起一个web server,这里我安装 [Web Server for Chrome](https://chrome.google.com/webstore/detail/web-server-for-chrome/ofhbbkphhbklhfoeikjpcbhemlocgigb),你也可以使用node或者上传至github托管。
33 |
34 | ### PC端调试
35 | #### 1. 安装chrome扩展程序 [WebVR API Emulation](https://chrome.google.com/webstore/detail/webvr-api-emulation/gbdnpaebafagioggnhkacnaaahpiefil)
36 | 使用WebVR API Emulation扩展程序可以模拟VR设备用户的视角、位置等。
37 |
38 | 
39 |
40 | ### 移动端调试
41 | 适用于cardboard级别的WebVR App调试。
42 | #### 1. 安装[chrome beta](https://play.google.com/store/apps/details?id=com.chrome.beta&hl=zh)
43 | 目前需要webvr还属于早期实验阶段,需要下载chrome beta最新版,安装完需要手动开启webvr支持,在浏览器地址栏输入`chrome://flags#enable-webvr`,点击启用并重新启动chrome。
44 |
45 |
46 | 
47 |
48 |
49 |
50 | #### 2. 安装[Google VR 服务](https://play.google.com/store/apps/details?id=com.google.vr.vrcore&hl=zh)
51 | 这是google给cardboard、daydream用户提供VR服务配置,可以提供VR模式窗口,如下图。
52 | 最后你可以在chrome上打开[WebVR示例页面](https://vrlist.io)验证是否配置成功
53 |
54 | 
55 | ###### 3. chrome inspector调试
56 | 通过手机chrome访问我们开发的WebVR页面,在PC端chrome输入`chrome://inspector`进行调试,具体可以参考 [远程调试 Android 设备使用入门](https://developers.google.com/web/tools/chrome-devtools/remote-debugging/?hl=zh-cn)。
57 |
58 | 完成WebVR开发环境配置之后,我们将正式进入WebVR开发之旅。
59 | >技术和框架:three.js for WebGL
60 |
61 | [Three.js](http://threejs.org)是构建3d场景的框架,它封装了WebGL函数,简化了创建场景的代码成本,利用three.js我们可以更优雅地创建出三维场景和三维动画,这里我使用的是0.86版本。
62 | 如果想了解纯WebGL开发WebVR应用以及WebVR具体环境配置,可以参考 [webvr教程--深度剖析](https://zhuanlan.zhihu.com/p/28324884)。
63 |
64 | > 需要引入的js插件:
65 | 1.[three.min.js](https://github.com/mrdoob/three.js/blob/dev/build/three.min.js)
66 | 2.[webvr-polyfill.js](https://github.com/googlevr/webvr-polyfill/) 由于[WebVR API](https://developer.mozilla.org/zh-CN/docs/Web/API/WebVR_API)还没被各大主流浏览器支持,因此需要引入它来解决兼容性问题。
67 |
68 |
69 | ### 3D场景构建
70 | ---
71 | 首先我们创建一个HTML文件
72 | ```html
73 |
74 |
75 |
76 |
77 |
78 | webVR-helloworld
79 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 | ```
97 | 接下来编写js脚本,开始创建我们的3d场景。
98 | #### 1.创建场景
99 |
100 | Three.js中的scene场景是绘制我们3d对象的整个容器
101 | ```javascript
102 | var scene = new THREE.Scene();
103 | ```
104 |
105 | #### 2.添加相机
106 |
107 | 
108 |
109 | Three.js中的camera相机代表用户的眼睛,我们通过设置FOV确定视野范围,
110 | ```javascript
111 | //定义一个60°的视角,视线范围在1到1000的透视相机
112 | var camera = new THREE. new THREE.PerspectiveCamera(60,window.innerWidth/window.innerHeight,1,1000);
113 | scene.add(camera);
114 | ```
115 |
116 | #### 3.添加渲染器
117 |
118 | Three.js的渲染器用来渲染camera所看到的画面
119 |
120 | ```javascript
121 | //初始化渲染器 antialias参数为ture表示开启抗锯齿策略
122 | var renderer = new THREE.WebGLRenderer({ antialias: true } );
123 | //设置渲染器渲染尺寸
124 | renderer.setSize(window.innerWidth,window.innerHeight);
125 | //设置渲染背景为白色
126 | renderer.setClearColor(0xeeeeee);
127 | //将渲染场景的canvas放入body标签里
128 | document.body.appendChild(renderer.domElement);
129 | ```
130 |
131 | ###### 4.添加一个立方体网格
132 |
133 | ```javascript
134 | // 创建立方体
135 | var geometry = new THREE.CubeGeometry( 10,10,10);
136 | var material = new THREE.MeshLambertMaterial( { color: 0xef6500,needsUpdate: true,opacity:1,transparent:true} );
137 | var cube = new THREE.Mesh( geometry, material );
138 | cube.position.set(0,100,-50);
139 | cube.rotation.set(Math.PI/6,Math.PI/4,0);
140 | scene.add(cube);
141 | ```
142 |
143 | #### 5.启动动画
144 |
145 | 动画渲染的原理:渲染器的持续调用绘制方法,方法里动态改变物体的属性。
146 | 旧版的three.js需要手动调用requestAnimationFrame()方法递归的方式来渲染动画,新版three.js已经封装了该属性,因此只需要通过渲染器`renderer.animate(callback)`。
147 |
148 | ```javascript
149 | function update() {
150 | //让立方体旋转
151 | cube.rotation.y += 0.01;
152 | //渲染器渲染场景,等同于给相机按下快门
153 | renderer.render(scene, camera);
154 | }
155 | renderer.animate(update);//启动动画
156 | ```
157 |
158 |
159 | 
160 |
161 | 至此,我们已经绘制了一个简单的3d场景并且让它动了起来,接下来,我们需要让我们的场景可以支持WebVR模式。
162 |
163 | ### WebVR场景开发
164 | ---
165 | WebVR网页开发的基本原理是通过WebVR API获取VR动态数据(VR Display frameData),渲染器根据VR数据来分别绘制左右屏场景,具体步骤如下:
166 | 1. 使用`navigator.getVRDisplays`获取vr设备示例
167 | 
168 | vrdisplay是vr设备的实例,我们需要将它传给当前运行的renderer渲染器。
169 | ```javascript
170 | function initVR(renderer) {
171 | renderer.vr.enabled = true;
172 | navigator.getVRDisplays().then( function(display) {
173 | renderer.vr.setDevice(display[0]);
174 | const button = document.querySelector('.vr-btn');
175 | VRbutton(display[0],renderer,button,function() {
176 | button.textContent = '退出VR';
177 | },function() {
178 | button.textContent = '进入VR';
179 | });
180 | }).catch(err => console.warn(err));
181 | }
182 |
183 | ```
184 | 这里需要通过按钮来控制当前的渲染模式:
185 | 1. 当点击按钮时,根据`display.isPresenting`判断当前是否是使用vr设备下进行渲染,如果false,进入2,否则true进入3
186 | 2. 当前非VR模式,点击按钮进入VR模式,此时调用`display.requestPresent()`,`display.isPresenting`被设置为true,触发window的`vrdisplaypresentchange`事件
187 | 3. 当前为VR模式,点击按钮退出模式,此时调用`display.exitPresent()`,`display.isPresenting`被设置为false,触发window的`vrdisplaypresentchange`事件
188 | ```javascript
189 | /** VR按钮控制
190 | * @param {VRDisplay} display VRDisplay实例
191 | * @param {THREE.WebGLRenderer} renderer 渲染器
192 | * @param {HTMLElement} button VR控制按钮
193 | * @param {Function} enterVR 点击进入VR模式时回调
194 | * @param {Function} exitVR 点击退出VR模式时回调
195 | **/
196 | function VRbutton(display,renderer,button,enterVR,exitVR) {
197 | if ( display ) {
198 | button.addEventListener('click', function() {
199 | // 点击vr按钮控制`isPresenting`状态
200 | display.isPresenting ? display.exitPresent() : display.requestPresent( [ { source: renderer.domElement } ] );
201 | });
202 |
203 | window.addEventListener( 'vrdisplaypresentchange', function() {
204 | // 是否处于vr体验模式中,是则触发enterVR,否则触发exitVR
205 | display.isPresenting ? enterVR() : exitVR();
206 | }, false );
207 |
208 | } else {
209 | // 找不到vr设备实例,则移除按钮
210 | button.remove();
211 | }
212 | }
213 |
214 | ```
215 | 我们可以在`vrdisplaypresentchange`事件中根据`isPresenting`的值来改变按钮的UI,而three.js将根据`isPresenting`的值来决定是常规渲染还是vr模式渲染,在vr模式下,three.js将创建两个camera进行渲染。
216 |
217 | ### 代码优化
218 | 最后,将WebVR应用写成ES6 class,后面开发流程将按如下图结构来规范代码:
219 |
220 | 
221 |
222 | 第一步,构造函数先初始化VR场景、相机和渲染器;
223 | 第二步,在渲染之前调用start方法,在start方法里我们为场景创建3d物体;
224 | 最后,调起`renderer.animate(this.update)`开启动画渲染,update方法里我们可动态操作物体属性,具体代码如下:
225 | ```javascript
226 | class WebVRApp {
227 | constructor() {
228 | // 初始化场景
229 | this.scene = new THREE.Scene();
230 | // 初始化相机
231 | this.camera = new THREE.PerspectiveCamera(60,window.innerWidth/window.innerHeight,0.1,1000);
232 | this.scene.add(this.camera);
233 |
234 | // 初始化渲染器
235 | this.renderer = new THREE.WebGLRenderer({ antialias: true } );
236 | this.renderer.setSize(window.innerWidth,window.innerHeight);
237 | this.renderer.setClearColor(0x519EcB);
238 | this.renderer.setPixelRatio(window.devicePixelRatio);
239 | document.querySelector('.main-page').appendChild(this.renderer.domElement);
240 |
241 | this.clock = new THREE.Clock();
242 | // VR初始化
243 | this._initVR();
244 | // 往场景添加3d物体
245 | this.start();
246 | // 窗口大小调整监听
247 | window.addEventListener( 'resize', this._resize.bind(this), false );
248 | // 渲染动画
249 | this.renderer.animate(this.update.bind(this));
250 | }
251 | // 创建3d物体
252 | start() {
253 | const { scene, camera } = this;
254 | // 创建光线、地面等
255 | ...
256 | // 创建立方体
257 | const geometry = new THREE.CubeGeometry(2, 2, 2);
258 | const material = new THREE.MeshLambertMaterial({
259 | color: 0xef6500,
260 | });
261 | this.cube = new THREE.Mesh( geometry, material );
262 | this.cube.position.set({ x: 0, y: 0, z: -4 });
263 | scene.add(this.cube);
264 | }
265 | // 动画更新
266 | update() {
267 | const {scene,camera,renderer,clock} = this;
268 | const delta = clock.getDelta() * 60;
269 | // 启动渲染
270 | this.cube.rotation.y += 0.1 * delta;
271 | renderer.render(scene, camera);
272 | }
273 | // VR模式初始化
274 | _initVR() {
275 | const { renderer } = this;
276 | renderer.vr.enabled = true;
277 | // 获取VRDisplay实例
278 | navigator.getVRDisplays().then( display => {
279 | // 将display实例传给renderer渲染器
280 | renderer.vr.setDevice(display[0]);
281 | const button = document.querySelector('.vr-btn');
282 | VRButton.init(display[0],renderer,button,() => button.textContent = '退出VR',() => button.textContent = '进入VR');
283 | }).catch(err => console.warn(err));
284 | }
285 | // 窗口调整监听
286 | _resize() {
287 | const { camera, renderer } = this;
288 | // 窗口调整重新调整渲染器
289 | camera.aspect = window.innerWidth / window.innerHeight;
290 | camera.updateProjectionMatrix();
291 | renderer.setSize(window.innerWidth, window.innerHeight);
292 | }
293 | }
294 | new WebVRApp();
295 | ```
296 | 
297 |
298 | 完整代码:[github.com/YoneChen/WebVR-helloworld](https://github.com/YoneChen/WebVR-helloworld)。
299 |
300 | ### 结语
301 | ---
302 | 目前,国外的谷歌、火狐、Facebook和国内百度已推出支持WebVR浏览器的版本,微软也宣布将推出自己的VR浏览器,随着后期5g网络极速时代的到来以及HMD头显的价格和平台的成熟,WebVR的体验方式将是革命性的,用户通过WebVR浏览网上商店,线上教学可进行“面对面”师生交流等,基于这种种应用场景,我们可以找到一个更好的动力去学习WebVR。
303 |
304 | ### 参考链接
305 | ---
306 | [responisve WebVR](http://smus.com/responsive-vr/): 探讨WebVR在不同头显(HMD)的适配方案
307 | [MolizaVR example](https://mozvr.com/#showcase): 火狐WebVR示例
308 | [webvr-boilerplate](https://github.com/borismus/webvr-boilerplate): A starting point for web-based VR experiences that work on all VR headsets.
309 | [how to build webvr](https://www.sitepoint.com/how-to-build-vr-on-the-web-today/): How to Build VR on the Web Today
310 |
311 |
--------------------------------------------------------------------------------
/audio/horn.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YoneChen/WebVR-helloworld/067a34dc835c128161c9ac23821869e4a36cd48f/audio/horn.wav
--------------------------------------------------------------------------------
/cardboard.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | cardboard-gaze
8 |
43 |
44 |
45 |
48 |
49 |
50 |
51 |
52 |
279 |
--------------------------------------------------------------------------------
/common/vrbase.js:
--------------------------------------------------------------------------------
1 |
2 | class VRBase {
3 | constructor({ renderElement,buttonElement }) {
4 | this.renderElement = renderElement;
5 | this.buttonElement = buttonElement;
6 | // 初始化场景
7 | this.scene = new THREE.Scene();
8 | // 初始化相机
9 | this.camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
10 | this.scene.add(this.camera);
11 |
12 | // 初始化渲染器
13 | this.renderer = new THREE.WebGLRenderer({ antialias: true });
14 | this.renderer.setSize(window.innerWidth, window.innerHeight);
15 | this.renderer.shadowMapEnabled = true;
16 | this.renderer.setPixelRatio(window.devicePixelRatio);
17 | renderElement.appendChild(this.renderer.domElement);
18 |
19 | this.clock = new THREE.Clock();
20 | // VR初始化
21 | this._initVR();
22 | // 往场景添加3d物体
23 | this.start();
24 | // 窗口大小调整监听
25 | window.addEventListener('resize', this._resize.bind(this), false);
26 | // THREE.DefaultLoadingManager.onLoad = () => {
27 | // 渲染动画
28 | this.renderer.animate(this._animate.bind(this));
29 | // }
30 | }
31 | start() {}
32 | update() {}
33 | _initVR() {
34 | const { renderer, buttonElement} = this;
35 | renderer.vr.enabled = true;
36 | // 获取VRDisplay实例
37 | navigator.getVRDisplays().then(display => {
38 | // 将display实例传给renderer渲染器
39 | renderer.vr.setDevice(display[0]);
40 | VRButton.init(display[0], renderer, buttonElement, () => buttonElement.textContent = '退出VR', () => buttonElement.textContent = '进入VR');
41 | }).catch(err => console.warn(err));
42 | }
43 | _resize() {
44 | const { camera, renderer } = this;
45 | // 窗口调整重新调整渲染器
46 | camera.aspect = window.innerWidth / window.innerHeight;
47 | camera.updateProjectionMatrix();
48 | renderer.setSize(window.innerWidth, window.innerHeight);
49 | }
50 | _animate() {
51 | const { scene, camera, renderer } = this;
52 | // 启动渲染
53 | this.update();
54 | renderer.render(scene, camera);
55 | }
56 | }
57 | // VR按钮控制
58 | const VRButton = {
59 | /**
60 | * @param {VRDisplay} display VRDisplay实例
61 | * @param {THREE.WebGLRenderer} renderer 渲染器
62 | * @param {HTMLElement} button VR控制按钮
63 | * @param {Function} enterVR 点击进入VR模式时回调
64 | * @param {Function} exitVR 点击退出VR模式时回调
65 | **/
66 | init(display, renderer, button, enterVR, exitVR) {
67 | if (display) {
68 | button.addEventListener('click', e => {
69 | display.isPresenting ? display.exitPresent() : display.requestPresent([{ source: renderer.domElement }]);
70 | });
71 | window.addEventListener('vrdisplaypresentchange', e => {
72 | display.isPresenting ? enterVR() : exitVR();
73 | }, false);
74 | } else {
75 | button.remove();
76 | }
77 | }
78 | }
--------------------------------------------------------------------------------
/examples/3d-audio.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | webVR-3d-audio
7 |
38 |
39 |
40 |
43 |
44 |
45 |
46 |
47 |
195 |
--------------------------------------------------------------------------------
/examples/cardboard2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | cardboard-gaze
7 |
38 |
39 |
40 |
43 |
44 |
45 |
46 |
47 |
48 |
266 |
--------------------------------------------------------------------------------
/examples/physics.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | physics-world
7 |
38 |
39 |
40 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
265 |
--------------------------------------------------------------------------------
/examples/voice.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | voice-interaction
7 |
38 |
39 |
40 |
43 |
44 |
45 |
46 |
47 |
187 |
--------------------------------------------------------------------------------
/gearvr.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | gearvr-touchpad
8 |
46 |
47 |
48 |
49 |
52 |
53 |
54 |
55 |
56 |
391 |
392 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | webVR-helloworld
7 |
24 |
25 |
26 |
27 | 教程
28 |
36 |
37 |
38 | 示例
39 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/texture/road.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YoneChen/WebVR-helloworld/067a34dc835c128161c9ac23821869e4a36cd48f/texture/road.jpg
--------------------------------------------------------------------------------
/vendor/Projector.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author mrdoob / http://mrdoob.com/
3 | * @author supereggbert / http://www.paulbrunt.co.uk/
4 | * @author julianwa / https://github.com/julianwa
5 | */
6 |
7 | THREE.RenderableObject = function () {
8 |
9 | this.id = 0;
10 |
11 | this.object = null;
12 | this.z = 0;
13 | this.renderOrder = 0;
14 |
15 | };
16 |
17 | //
18 |
19 | THREE.RenderableFace = function () {
20 |
21 | this.id = 0;
22 |
23 | this.v1 = new THREE.RenderableVertex();
24 | this.v2 = new THREE.RenderableVertex();
25 | this.v3 = new THREE.RenderableVertex();
26 |
27 | this.normalModel = new THREE.Vector3();
28 |
29 | this.vertexNormalsModel = [ new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3() ];
30 | this.vertexNormalsLength = 0;
31 |
32 | this.color = new THREE.Color();
33 | this.material = null;
34 | this.uvs = [ new THREE.Vector2(), new THREE.Vector2(), new THREE.Vector2() ];
35 |
36 | this.z = 0;
37 | this.renderOrder = 0;
38 |
39 | };
40 |
41 | //
42 |
43 | THREE.RenderableVertex = function () {
44 |
45 | this.position = new THREE.Vector3();
46 | this.positionWorld = new THREE.Vector3();
47 | this.positionScreen = new THREE.Vector4();
48 |
49 | this.visible = true;
50 |
51 | };
52 |
53 | THREE.RenderableVertex.prototype.copy = function ( vertex ) {
54 |
55 | this.positionWorld.copy( vertex.positionWorld );
56 | this.positionScreen.copy( vertex.positionScreen );
57 |
58 | };
59 |
60 | //
61 |
62 | THREE.RenderableLine = function () {
63 |
64 | this.id = 0;
65 |
66 | this.v1 = new THREE.RenderableVertex();
67 | this.v2 = new THREE.RenderableVertex();
68 |
69 | this.vertexColors = [ new THREE.Color(), new THREE.Color() ];
70 | this.material = null;
71 |
72 | this.z = 0;
73 | this.renderOrder = 0;
74 |
75 | };
76 |
77 | //
78 |
79 | THREE.RenderableSprite = function () {
80 |
81 | this.id = 0;
82 |
83 | this.object = null;
84 |
85 | this.x = 0;
86 | this.y = 0;
87 | this.z = 0;
88 |
89 | this.rotation = 0;
90 | this.scale = new THREE.Vector2();
91 |
92 | this.material = null;
93 | this.renderOrder = 0;
94 |
95 | };
96 |
97 | //
98 |
99 | THREE.Projector = function () {
100 |
101 | var _object, _objectCount, _objectPool = [], _objectPoolLength = 0,
102 | _vertex, _vertexCount, _vertexPool = [], _vertexPoolLength = 0,
103 | _face, _faceCount, _facePool = [], _facePoolLength = 0,
104 | _line, _lineCount, _linePool = [], _linePoolLength = 0,
105 | _sprite, _spriteCount, _spritePool = [], _spritePoolLength = 0,
106 |
107 | _renderData = { objects: [], lights: [], elements: [] },
108 |
109 | _vector3 = new THREE.Vector3(),
110 | _vector4 = new THREE.Vector4(),
111 |
112 | _clipBox = new THREE.Box3( new THREE.Vector3( - 1, - 1, - 1 ), new THREE.Vector3( 1, 1, 1 ) ),
113 | _boundingBox = new THREE.Box3(),
114 | _points3 = new Array( 3 ),
115 | _points4 = new Array( 4 ),
116 |
117 | _viewMatrix = new THREE.Matrix4(),
118 | _viewProjectionMatrix = new THREE.Matrix4(),
119 |
120 | _modelMatrix,
121 | _modelViewProjectionMatrix = new THREE.Matrix4(),
122 |
123 | _normalMatrix = new THREE.Matrix3(),
124 |
125 | _frustum = new THREE.Frustum(),
126 |
127 | _clippedVertex1PositionScreen = new THREE.Vector4(),
128 | _clippedVertex2PositionScreen = new THREE.Vector4();
129 |
130 | //
131 |
132 | this.projectVector = function ( vector, camera ) {
133 |
134 | console.warn( 'THREE.Projector: .projectVector() is now vector.project().' );
135 | vector.project( camera );
136 |
137 | };
138 |
139 | this.unprojectVector = function ( vector, camera ) {
140 |
141 | console.warn( 'THREE.Projector: .unprojectVector() is now vector.unproject().' );
142 | vector.unproject( camera );
143 |
144 | };
145 |
146 | this.pickingRay = function ( vector, camera ) {
147 |
148 | console.error( 'THREE.Projector: .pickingRay() is now raycaster.setFromCamera().' );
149 |
150 | };
151 |
152 | //
153 |
154 | var RenderList = function () {
155 |
156 | var normals = [];
157 | var uvs = [];
158 |
159 | var object = null;
160 | var material = null;
161 |
162 | var normalMatrix = new THREE.Matrix3();
163 |
164 | function setObject( value ) {
165 |
166 | object = value;
167 | material = object.material;
168 |
169 | normalMatrix.getNormalMatrix( object.matrixWorld );
170 |
171 | normals.length = 0;
172 | uvs.length = 0;
173 |
174 | }
175 |
176 | function projectVertex( vertex ) {
177 |
178 | var position = vertex.position;
179 | var positionWorld = vertex.positionWorld;
180 | var positionScreen = vertex.positionScreen;
181 |
182 | positionWorld.copy( position ).applyMatrix4( _modelMatrix );
183 | positionScreen.copy( positionWorld ).applyMatrix4( _viewProjectionMatrix );
184 |
185 | var invW = 1 / positionScreen.w;
186 |
187 | positionScreen.x *= invW;
188 | positionScreen.y *= invW;
189 | positionScreen.z *= invW;
190 |
191 | vertex.visible = positionScreen.x >= - 1 && positionScreen.x <= 1 &&
192 | positionScreen.y >= - 1 && positionScreen.y <= 1 &&
193 | positionScreen.z >= - 1 && positionScreen.z <= 1;
194 |
195 | }
196 |
197 | function pushVertex( x, y, z ) {
198 |
199 | _vertex = getNextVertexInPool();
200 | _vertex.position.set( x, y, z );
201 |
202 | projectVertex( _vertex );
203 |
204 | }
205 |
206 | function pushNormal( x, y, z ) {
207 |
208 | normals.push( x, y, z );
209 |
210 | }
211 |
212 | function pushUv( x, y ) {
213 |
214 | uvs.push( x, y );
215 |
216 | }
217 |
218 | function checkTriangleVisibility( v1, v2, v3 ) {
219 |
220 | if ( v1.visible === true || v2.visible === true || v3.visible === true ) return true;
221 |
222 | _points3[ 0 ] = v1.positionScreen;
223 | _points3[ 1 ] = v2.positionScreen;
224 | _points3[ 2 ] = v3.positionScreen;
225 |
226 | return _clipBox.intersectsBox( _boundingBox.setFromPoints( _points3 ) );
227 |
228 | }
229 |
230 | function checkBackfaceCulling( v1, v2, v3 ) {
231 |
232 | return ( ( v3.positionScreen.x - v1.positionScreen.x ) *
233 | ( v2.positionScreen.y - v1.positionScreen.y ) -
234 | ( v3.positionScreen.y - v1.positionScreen.y ) *
235 | ( v2.positionScreen.x - v1.positionScreen.x ) ) < 0;
236 |
237 | }
238 |
239 | function pushLine( a, b ) {
240 |
241 | var v1 = _vertexPool[ a ];
242 | var v2 = _vertexPool[ b ];
243 |
244 | _line = getNextLineInPool();
245 |
246 | _line.id = object.id;
247 | _line.v1.copy( v1 );
248 | _line.v2.copy( v2 );
249 | _line.z = ( v1.positionScreen.z + v2.positionScreen.z ) / 2;
250 | _line.renderOrder = object.renderOrder;
251 |
252 | _line.material = object.material;
253 |
254 | _renderData.elements.push( _line );
255 |
256 | }
257 |
258 | function pushTriangle( a, b, c ) {
259 |
260 | var v1 = _vertexPool[ a ];
261 | var v2 = _vertexPool[ b ];
262 | var v3 = _vertexPool[ c ];
263 |
264 | if ( checkTriangleVisibility( v1, v2, v3 ) === false ) return;
265 |
266 | if ( material.side === THREE.DoubleSide || checkBackfaceCulling( v1, v2, v3 ) === true ) {
267 |
268 | _face = getNextFaceInPool();
269 |
270 | _face.id = object.id;
271 | _face.v1.copy( v1 );
272 | _face.v2.copy( v2 );
273 | _face.v3.copy( v3 );
274 | _face.z = ( v1.positionScreen.z + v2.positionScreen.z + v3.positionScreen.z ) / 3;
275 | _face.renderOrder = object.renderOrder;
276 |
277 | // use first vertex normal as face normal
278 |
279 | _face.normalModel.fromArray( normals, a * 3 );
280 | _face.normalModel.applyMatrix3( normalMatrix ).normalize();
281 |
282 | for ( var i = 0; i < 3; i ++ ) {
283 |
284 | var normal = _face.vertexNormalsModel[ i ];
285 | normal.fromArray( normals, arguments[ i ] * 3 );
286 | normal.applyMatrix3( normalMatrix ).normalize();
287 |
288 | var uv = _face.uvs[ i ];
289 | uv.fromArray( uvs, arguments[ i ] * 2 );
290 |
291 | }
292 |
293 | _face.vertexNormalsLength = 3;
294 |
295 | _face.material = object.material;
296 |
297 | _renderData.elements.push( _face );
298 |
299 | }
300 |
301 | }
302 |
303 | return {
304 | setObject: setObject,
305 | projectVertex: projectVertex,
306 | checkTriangleVisibility: checkTriangleVisibility,
307 | checkBackfaceCulling: checkBackfaceCulling,
308 | pushVertex: pushVertex,
309 | pushNormal: pushNormal,
310 | pushUv: pushUv,
311 | pushLine: pushLine,
312 | pushTriangle: pushTriangle
313 | }
314 |
315 | };
316 |
317 | var renderList = new RenderList();
318 |
319 | this.projectScene = function ( scene, camera, sortObjects, sortElements ) {
320 |
321 | _faceCount = 0;
322 | _lineCount = 0;
323 | _spriteCount = 0;
324 |
325 | _renderData.elements.length = 0;
326 |
327 | if ( scene.autoUpdate === true ) scene.updateMatrixWorld();
328 | if ( camera.parent === null ) camera.updateMatrixWorld();
329 |
330 | _viewMatrix.copy( camera.matrixWorldInverse.getInverse( camera.matrixWorld ) );
331 | _viewProjectionMatrix.multiplyMatrices( camera.projectionMatrix, _viewMatrix );
332 |
333 | _frustum.setFromMatrix( _viewProjectionMatrix );
334 |
335 | //
336 |
337 | _objectCount = 0;
338 |
339 | _renderData.objects.length = 0;
340 | _renderData.lights.length = 0;
341 |
342 | function addObject( object ) {
343 |
344 | _object = getNextObjectInPool();
345 | _object.id = object.id;
346 | _object.object = object;
347 |
348 | _vector3.setFromMatrixPosition( object.matrixWorld );
349 | _vector3.applyProjection( _viewProjectionMatrix );
350 | _object.z = _vector3.z;
351 | _object.renderOrder = object.renderOrder;
352 |
353 | _renderData.objects.push( _object );
354 |
355 | }
356 |
357 | scene.traverseVisible( function ( object ) {
358 |
359 | if ( object instanceof THREE.Light ) {
360 |
361 | _renderData.lights.push( object );
362 |
363 | } else if ( object instanceof THREE.Mesh || object instanceof THREE.Line ) {
364 |
365 | if ( object.material.visible === false ) return;
366 | if ( object.frustumCulled === true && _frustum.intersectsObject( object ) === false ) return;
367 |
368 | addObject( object );
369 |
370 | } else if ( object instanceof THREE.Sprite ) {
371 |
372 | if ( object.material.visible === false ) return;
373 | if ( object.frustumCulled === true && _frustum.intersectsSprite( object ) === false ) return;
374 |
375 | addObject( object );
376 |
377 | }
378 |
379 | } );
380 |
381 | if ( sortObjects === true ) {
382 |
383 | _renderData.objects.sort( painterSort );
384 |
385 | }
386 |
387 | //
388 |
389 | for ( var o = 0, ol = _renderData.objects.length; o < ol; o ++ ) {
390 |
391 | var object = _renderData.objects[ o ].object;
392 | var geometry = object.geometry;
393 |
394 | renderList.setObject( object );
395 |
396 | _modelMatrix = object.matrixWorld;
397 |
398 | _vertexCount = 0;
399 |
400 | if ( object instanceof THREE.Mesh ) {
401 |
402 | if ( geometry instanceof THREE.BufferGeometry ) {
403 |
404 | var attributes = geometry.attributes;
405 | var groups = geometry.groups;
406 |
407 | if ( attributes.position === undefined ) continue;
408 |
409 | var positions = attributes.position.array;
410 |
411 | for ( var i = 0, l = positions.length; i < l; i += 3 ) {
412 |
413 | renderList.pushVertex( positions[ i ], positions[ i + 1 ], positions[ i + 2 ] );
414 |
415 | }
416 |
417 | if ( attributes.normal !== undefined ) {
418 |
419 | var normals = attributes.normal.array;
420 |
421 | for ( var i = 0, l = normals.length; i < l; i += 3 ) {
422 |
423 | renderList.pushNormal( normals[ i ], normals[ i + 1 ], normals[ i + 2 ] );
424 |
425 | }
426 |
427 | }
428 |
429 | if ( attributes.uv !== undefined ) {
430 |
431 | var uvs = attributes.uv.array;
432 |
433 | for ( var i = 0, l = uvs.length; i < l; i += 2 ) {
434 |
435 | renderList.pushUv( uvs[ i ], uvs[ i + 1 ] );
436 |
437 | }
438 |
439 | }
440 |
441 | if ( geometry.index !== null ) {
442 |
443 | var indices = geometry.index.array;
444 |
445 | if ( groups.length > 0 ) {
446 |
447 | for ( var o = 0; o < groups.length; o ++ ) {
448 |
449 | var group = groups[ o ];
450 |
451 | for ( var i = group.start, l = group.start + group.count; i < l; i += 3 ) {
452 |
453 | renderList.pushTriangle( indices[ i ], indices[ i + 1 ], indices[ i + 2 ] );
454 |
455 | }
456 |
457 | }
458 |
459 | } else {
460 |
461 | for ( var i = 0, l = indices.length; i < l; i += 3 ) {
462 |
463 | renderList.pushTriangle( indices[ i ], indices[ i + 1 ], indices[ i + 2 ] );
464 |
465 | }
466 |
467 | }
468 |
469 | } else {
470 |
471 | for ( var i = 0, l = positions.length / 3; i < l; i += 3 ) {
472 |
473 | renderList.pushTriangle( i, i + 1, i + 2 );
474 |
475 | }
476 |
477 | }
478 |
479 | } else if ( geometry instanceof THREE.Geometry ) {
480 |
481 | var vertices = geometry.vertices;
482 | var faces = geometry.faces;
483 | var faceVertexUvs = geometry.faceVertexUvs[ 0 ];
484 |
485 | _normalMatrix.getNormalMatrix( _modelMatrix );
486 |
487 | var material = object.material;
488 |
489 | var isFaceMaterial = material instanceof THREE.MultiMaterial;
490 | var objectMaterials = isFaceMaterial === true ? object.material : null;
491 |
492 | for ( var v = 0, vl = vertices.length; v < vl; v ++ ) {
493 |
494 | var vertex = vertices[ v ];
495 |
496 | _vector3.copy( vertex );
497 |
498 | if ( material.morphTargets === true ) {
499 |
500 | var morphTargets = geometry.morphTargets;
501 | var morphInfluences = object.morphTargetInfluences;
502 |
503 | for ( var t = 0, tl = morphTargets.length; t < tl; t ++ ) {
504 |
505 | var influence = morphInfluences[ t ];
506 |
507 | if ( influence === 0 ) continue;
508 |
509 | var target = morphTargets[ t ];
510 | var targetVertex = target.vertices[ v ];
511 |
512 | _vector3.x += ( targetVertex.x - vertex.x ) * influence;
513 | _vector3.y += ( targetVertex.y - vertex.y ) * influence;
514 | _vector3.z += ( targetVertex.z - vertex.z ) * influence;
515 |
516 | }
517 |
518 | }
519 |
520 | renderList.pushVertex( _vector3.x, _vector3.y, _vector3.z );
521 |
522 | }
523 |
524 | for ( var f = 0, fl = faces.length; f < fl; f ++ ) {
525 |
526 | var face = faces[ f ];
527 |
528 | material = isFaceMaterial === true
529 | ? objectMaterials.materials[ face.materialIndex ]
530 | : object.material;
531 |
532 | if ( material === undefined ) continue;
533 |
534 | var side = material.side;
535 |
536 | var v1 = _vertexPool[ face.a ];
537 | var v2 = _vertexPool[ face.b ];
538 | var v3 = _vertexPool[ face.c ];
539 |
540 | if ( renderList.checkTriangleVisibility( v1, v2, v3 ) === false ) continue;
541 |
542 | var visible = renderList.checkBackfaceCulling( v1, v2, v3 );
543 |
544 | if ( side !== THREE.DoubleSide ) {
545 |
546 | if ( side === THREE.FrontSide && visible === false ) continue;
547 | if ( side === THREE.BackSide && visible === true ) continue;
548 |
549 | }
550 |
551 | _face = getNextFaceInPool();
552 |
553 | _face.id = object.id;
554 | _face.v1.copy( v1 );
555 | _face.v2.copy( v2 );
556 | _face.v3.copy( v3 );
557 |
558 | _face.normalModel.copy( face.normal );
559 |
560 | if ( visible === false && ( side === THREE.BackSide || side === THREE.DoubleSide ) ) {
561 |
562 | _face.normalModel.negate();
563 |
564 | }
565 |
566 | _face.normalModel.applyMatrix3( _normalMatrix ).normalize();
567 |
568 | var faceVertexNormals = face.vertexNormals;
569 |
570 | for ( var n = 0, nl = Math.min( faceVertexNormals.length, 3 ); n < nl; n ++ ) {
571 |
572 | var normalModel = _face.vertexNormalsModel[ n ];
573 | normalModel.copy( faceVertexNormals[ n ] );
574 |
575 | if ( visible === false && ( side === THREE.BackSide || side === THREE.DoubleSide ) ) {
576 |
577 | normalModel.negate();
578 |
579 | }
580 |
581 | normalModel.applyMatrix3( _normalMatrix ).normalize();
582 |
583 | }
584 |
585 | _face.vertexNormalsLength = faceVertexNormals.length;
586 |
587 | var vertexUvs = faceVertexUvs[ f ];
588 |
589 | if ( vertexUvs !== undefined ) {
590 |
591 | for ( var u = 0; u < 3; u ++ ) {
592 |
593 | _face.uvs[ u ].copy( vertexUvs[ u ] );
594 |
595 | }
596 |
597 | }
598 |
599 | _face.color = face.color;
600 | _face.material = material;
601 |
602 | _face.z = ( v1.positionScreen.z + v2.positionScreen.z + v3.positionScreen.z ) / 3;
603 | _face.renderOrder = object.renderOrder;
604 |
605 | _renderData.elements.push( _face );
606 |
607 | }
608 |
609 | }
610 |
611 | } else if ( object instanceof THREE.Line ) {
612 |
613 | if ( geometry instanceof THREE.BufferGeometry ) {
614 |
615 | var attributes = geometry.attributes;
616 |
617 | if ( attributes.position !== undefined ) {
618 |
619 | var positions = attributes.position.array;
620 |
621 | for ( var i = 0, l = positions.length; i < l; i += 3 ) {
622 |
623 | renderList.pushVertex( positions[ i ], positions[ i + 1 ], positions[ i + 2 ] );
624 |
625 | }
626 |
627 | if ( geometry.index !== null ) {
628 |
629 | var indices = geometry.index.array;
630 |
631 | for ( var i = 0, l = indices.length; i < l; i += 2 ) {
632 |
633 | renderList.pushLine( indices[ i ], indices[ i + 1 ] );
634 |
635 | }
636 |
637 | } else {
638 |
639 | var step = object instanceof THREE.LineSegments ? 2 : 1;
640 |
641 | for ( var i = 0, l = ( positions.length / 3 ) - 1; i < l; i += step ) {
642 |
643 | renderList.pushLine( i, i + 1 );
644 |
645 | }
646 |
647 | }
648 |
649 | }
650 |
651 | } else if ( geometry instanceof THREE.Geometry ) {
652 |
653 | _modelViewProjectionMatrix.multiplyMatrices( _viewProjectionMatrix, _modelMatrix );
654 |
655 | var vertices = object.geometry.vertices;
656 |
657 | if ( vertices.length === 0 ) continue;
658 |
659 | v1 = getNextVertexInPool();
660 | v1.positionScreen.copy( vertices[ 0 ] ).applyMatrix4( _modelViewProjectionMatrix );
661 |
662 | var step = object instanceof THREE.LineSegments ? 2 : 1;
663 |
664 | for ( var v = 1, vl = vertices.length; v < vl; v ++ ) {
665 |
666 | v1 = getNextVertexInPool();
667 | v1.positionScreen.copy( vertices[ v ] ).applyMatrix4( _modelViewProjectionMatrix );
668 |
669 | if ( ( v + 1 ) % step > 0 ) continue;
670 |
671 | v2 = _vertexPool[ _vertexCount - 2 ];
672 |
673 | _clippedVertex1PositionScreen.copy( v1.positionScreen );
674 | _clippedVertex2PositionScreen.copy( v2.positionScreen );
675 |
676 | if ( clipLine( _clippedVertex1PositionScreen, _clippedVertex2PositionScreen ) === true ) {
677 |
678 | // Perform the perspective divide
679 | _clippedVertex1PositionScreen.multiplyScalar( 1 / _clippedVertex1PositionScreen.w );
680 | _clippedVertex2PositionScreen.multiplyScalar( 1 / _clippedVertex2PositionScreen.w );
681 |
682 | _line = getNextLineInPool();
683 |
684 | _line.id = object.id;
685 | _line.v1.positionScreen.copy( _clippedVertex1PositionScreen );
686 | _line.v2.positionScreen.copy( _clippedVertex2PositionScreen );
687 |
688 | _line.z = Math.max( _clippedVertex1PositionScreen.z, _clippedVertex2PositionScreen.z );
689 | _line.renderOrder = object.renderOrder;
690 |
691 | _line.material = object.material;
692 |
693 | if ( object.material.vertexColors === THREE.VertexColors ) {
694 |
695 | _line.vertexColors[ 0 ].copy( object.geometry.colors[ v ] );
696 | _line.vertexColors[ 1 ].copy( object.geometry.colors[ v - 1 ] );
697 |
698 | }
699 |
700 | _renderData.elements.push( _line );
701 |
702 | }
703 |
704 | }
705 |
706 | }
707 |
708 | } else if ( object instanceof THREE.Sprite ) {
709 |
710 | _vector4.set( _modelMatrix.elements[ 12 ], _modelMatrix.elements[ 13 ], _modelMatrix.elements[ 14 ], 1 );
711 | _vector4.applyMatrix4( _viewProjectionMatrix );
712 |
713 | var invW = 1 / _vector4.w;
714 |
715 | _vector4.z *= invW;
716 |
717 | if ( _vector4.z >= - 1 && _vector4.z <= 1 ) {
718 |
719 | _sprite = getNextSpriteInPool();
720 | _sprite.id = object.id;
721 | _sprite.x = _vector4.x * invW;
722 | _sprite.y = _vector4.y * invW;
723 | _sprite.z = _vector4.z;
724 | _sprite.renderOrder = object.renderOrder;
725 | _sprite.object = object;
726 |
727 | _sprite.rotation = object.rotation;
728 |
729 | _sprite.scale.x = object.scale.x * Math.abs( _sprite.x - ( _vector4.x + camera.projectionMatrix.elements[ 0 ] ) / ( _vector4.w + camera.projectionMatrix.elements[ 12 ] ) );
730 | _sprite.scale.y = object.scale.y * Math.abs( _sprite.y - ( _vector4.y + camera.projectionMatrix.elements[ 5 ] ) / ( _vector4.w + camera.projectionMatrix.elements[ 13 ] ) );
731 |
732 | _sprite.material = object.material;
733 |
734 | _renderData.elements.push( _sprite );
735 |
736 | }
737 |
738 | }
739 |
740 | }
741 |
742 | if ( sortElements === true ) {
743 |
744 | _renderData.elements.sort( painterSort );
745 |
746 | }
747 |
748 | return _renderData;
749 |
750 | };
751 |
752 | // Pools
753 |
754 | function getNextObjectInPool() {
755 |
756 | if ( _objectCount === _objectPoolLength ) {
757 |
758 | var object = new THREE.RenderableObject();
759 | _objectPool.push( object );
760 | _objectPoolLength ++;
761 | _objectCount ++;
762 | return object;
763 |
764 | }
765 |
766 | return _objectPool[ _objectCount ++ ];
767 |
768 | }
769 |
770 | function getNextVertexInPool() {
771 |
772 | if ( _vertexCount === _vertexPoolLength ) {
773 |
774 | var vertex = new THREE.RenderableVertex();
775 | _vertexPool.push( vertex );
776 | _vertexPoolLength ++;
777 | _vertexCount ++;
778 | return vertex;
779 |
780 | }
781 |
782 | return _vertexPool[ _vertexCount ++ ];
783 |
784 | }
785 |
786 | function getNextFaceInPool() {
787 |
788 | if ( _faceCount === _facePoolLength ) {
789 |
790 | var face = new THREE.RenderableFace();
791 | _facePool.push( face );
792 | _facePoolLength ++;
793 | _faceCount ++;
794 | return face;
795 |
796 | }
797 |
798 | return _facePool[ _faceCount ++ ];
799 |
800 |
801 | }
802 |
803 | function getNextLineInPool() {
804 |
805 | if ( _lineCount === _linePoolLength ) {
806 |
807 | var line = new THREE.RenderableLine();
808 | _linePool.push( line );
809 | _linePoolLength ++;
810 | _lineCount ++;
811 | return line;
812 |
813 | }
814 |
815 | return _linePool[ _lineCount ++ ];
816 |
817 | }
818 |
819 | function getNextSpriteInPool() {
820 |
821 | if ( _spriteCount === _spritePoolLength ) {
822 |
823 | var sprite = new THREE.RenderableSprite();
824 | _spritePool.push( sprite );
825 | _spritePoolLength ++;
826 | _spriteCount ++;
827 | return sprite;
828 |
829 | }
830 |
831 | return _spritePool[ _spriteCount ++ ];
832 |
833 | }
834 |
835 | //
836 |
837 | function painterSort( a, b ) {
838 |
839 | if ( a.renderOrder !== b.renderOrder ) {
840 |
841 | return a.renderOrder - b.renderOrder;
842 |
843 | } else if ( a.z !== b.z ) {
844 |
845 | return b.z - a.z;
846 |
847 | } else if ( a.id !== b.id ) {
848 |
849 | return a.id - b.id;
850 |
851 | } else {
852 |
853 | return 0;
854 |
855 | }
856 |
857 | }
858 |
859 | function clipLine( s1, s2 ) {
860 |
861 | var alpha1 = 0, alpha2 = 1,
862 |
863 | // Calculate the boundary coordinate of each vertex for the near and far clip planes,
864 | // Z = -1 and Z = +1, respectively.
865 | bc1near = s1.z + s1.w,
866 | bc2near = s2.z + s2.w,
867 | bc1far = - s1.z + s1.w,
868 | bc2far = - s2.z + s2.w;
869 |
870 | if ( bc1near >= 0 && bc2near >= 0 && bc1far >= 0 && bc2far >= 0 ) {
871 |
872 | // Both vertices lie entirely within all clip planes.
873 | return true;
874 |
875 | } else if ( ( bc1near < 0 && bc2near < 0 ) || ( bc1far < 0 && bc2far < 0 ) ) {
876 |
877 | // Both vertices lie entirely outside one of the clip planes.
878 | return false;
879 |
880 | } else {
881 |
882 | // The line segment spans at least one clip plane.
883 |
884 | if ( bc1near < 0 ) {
885 |
886 | // v1 lies outside the near plane, v2 inside
887 | alpha1 = Math.max( alpha1, bc1near / ( bc1near - bc2near ) );
888 |
889 | } else if ( bc2near < 0 ) {
890 |
891 | // v2 lies outside the near plane, v1 inside
892 | alpha2 = Math.min( alpha2, bc1near / ( bc1near - bc2near ) );
893 |
894 | }
895 |
896 | if ( bc1far < 0 ) {
897 |
898 | // v1 lies outside the far plane, v2 inside
899 | alpha1 = Math.max( alpha1, bc1far / ( bc1far - bc2far ) );
900 |
901 | } else if ( bc2far < 0 ) {
902 |
903 | // v2 lies outside the far plane, v2 inside
904 | alpha2 = Math.min( alpha2, bc1far / ( bc1far - bc2far ) );
905 |
906 | }
907 |
908 | if ( alpha2 < alpha1 ) {
909 |
910 | // The line segment spans two boundaries, but is outside both of them.
911 | // (This can't happen when we're only clipping against just near/far but good
912 | // to leave the check here for future usage if other clip planes are added.)
913 | return false;
914 |
915 | } else {
916 |
917 | // Update the s1 and s2 vertices to match the clipped line segment.
918 | s1.lerp( s2, alpha1 );
919 | s2.lerp( s1, 1 - alpha2 );
920 |
921 | return true;
922 |
923 | }
924 |
925 | }
926 |
927 | }
928 |
929 | };
930 |
--------------------------------------------------------------------------------
/vendor/VRControls.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author dmarcos / https://github.com/dmarcos
3 | * @author mrdoob / http://mrdoob.com
4 | */
5 |
6 | THREE.VRControls = function ( object, onError ) {
7 |
8 | var scope = this;
9 |
10 | var vrInputs = [];
11 |
12 | function filterInvalidDevices( devices ) {
13 |
14 | // Exclude Cardboard position sensor if Oculus exists.
15 |
16 | var oculusDevices = devices.filter( function ( device ) {
17 |
18 | return device.deviceName.toLowerCase().indexOf( 'oculus' ) !== - 1;
19 |
20 | } );
21 |
22 | if ( oculusDevices.length >= 1 ) {
23 |
24 | return devices.filter( function ( device ) {
25 |
26 | return device.deviceName.toLowerCase().indexOf( 'cardboard' ) === - 1;
27 |
28 | } );
29 |
30 | } else {
31 |
32 | return devices;
33 |
34 | }
35 |
36 | }
37 |
38 | function gotVRDevices( devices ) {
39 |
40 | devices = filterInvalidDevices( devices );
41 |
42 | for ( var i = 0; i < devices.length; i ++ ) {
43 |
44 | if ( devices[ i ] instanceof PositionSensorVRDevice ) {
45 |
46 | vrInputs.push( devices[ i ] );
47 |
48 | }
49 |
50 | }
51 |
52 | if ( onError ) onError( 'HMD not available' );
53 |
54 | }
55 |
56 | if ( navigator.getVRDevices ) {
57 |
58 | navigator.getVRDevices().then( gotVRDevices );
59 |
60 | }
61 |
62 | // the Rift SDK returns the position in meters
63 | // this scale factor allows the user to define how meters
64 | // are converted to scene units.
65 |
66 | this.scale = 1;
67 |
68 | this.update = function () {
69 |
70 | for ( var i = 0; i < vrInputs.length; i ++ ) {
71 |
72 | var vrInput = vrInputs[ i ];
73 |
74 | var state = vrInput.getState();
75 |
76 | if ( state.orientation !== null ) {
77 |
78 | object.quaternion.copy( state.orientation );
79 |
80 | }
81 |
82 | if ( state.position !== null ) {
83 |
84 | object.position.copy( state.position ).multiplyScalar( scope.scale );
85 |
86 | }
87 |
88 | }
89 |
90 | };
91 |
92 | this.resetSensor = function () {
93 |
94 | for ( var i = 0; i < vrInputs.length; i ++ ) {
95 |
96 | var vrInput = vrInputs[ i ];
97 |
98 | if ( vrInput.resetSensor !== undefined ) {
99 |
100 | vrInput.resetSensor();
101 |
102 | } else if ( vrInput.zeroSensor !== undefined ) {
103 |
104 | vrInput.zeroSensor();
105 |
106 | }
107 |
108 | }
109 |
110 | };
111 |
112 | this.zeroSensor = function () {
113 |
114 | console.warn( 'THREE.VRControls: .zeroSensor() is now .resetSensor().' );
115 | this.resetSensor();
116 |
117 | };
118 |
119 | this.dispose = function () {
120 |
121 | vrInputs = [];
122 |
123 | };
124 |
125 | };
126 |
--------------------------------------------------------------------------------
/vendor/VREffect.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author dmarcos / https://github.com/dmarcos
3 | * @author mrdoob / http://mrdoob.com
4 | *
5 | * WebVR Spec: http://mozvr.github.io/webvr-spec/webvr.html
6 | *
7 | * Firefox: http://mozvr.com/downloads/
8 | * Chromium: https://drive.google.com/folderview?id=0BzudLt22BqGRbW9WTHMtOWMzNjQ&usp=sharing#list
9 | *
10 | */
11 |
12 | THREE.VREffect = function ( renderer, onError ) {
13 |
14 | var vrHMD;
15 | var eyeTranslationL, eyeFOVL;
16 | var eyeTranslationR, eyeFOVR;
17 |
18 | function gotVRDevices( devices ) {
19 |
20 | for ( var i = 0; i < devices.length; i ++ ) {
21 |
22 | if ( devices[ i ] instanceof HMDVRDevice ) {
23 |
24 | vrHMD = devices[ i ];
25 |
26 | if ( vrHMD.getEyeParameters !== undefined ) {
27 |
28 | var eyeParamsL = vrHMD.getEyeParameters( 'left' );
29 | var eyeParamsR = vrHMD.getEyeParameters( 'right' );
30 |
31 | eyeTranslationL = eyeParamsL.eyeTranslation;
32 | eyeTranslationR = eyeParamsR.eyeTranslation;
33 | eyeFOVL = eyeParamsL.recommendedFieldOfView;
34 | eyeFOVR = eyeParamsR.recommendedFieldOfView;
35 |
36 | } else {
37 |
38 | // TODO: This is an older code path and not spec compliant.
39 | // It should be removed at some point in the near future.
40 | eyeTranslationL = vrHMD.getEyeTranslation( 'left' );
41 | eyeTranslationR = vrHMD.getEyeTranslation( 'right' );
42 | eyeFOVL = vrHMD.getRecommendedEyeFieldOfView( 'left' );
43 | eyeFOVR = vrHMD.getRecommendedEyeFieldOfView( 'right' );
44 |
45 | }
46 |
47 | break; // We keep the first we encounter
48 |
49 | }
50 |
51 | }
52 |
53 | if ( vrHMD === undefined ) {
54 |
55 | if ( onError ) onError( 'HMD not available' );
56 |
57 | }
58 |
59 | }
60 |
61 | if ( navigator.getVRDevices ) {
62 |
63 | navigator.getVRDevices().then( gotVRDevices );
64 |
65 | }
66 |
67 | //
68 |
69 | this.scale = 1;
70 |
71 | this.setSize = function( width, height ) {
72 |
73 | renderer.setSize( width, height );
74 |
75 | };
76 |
77 | // fullscreen
78 |
79 | var isFullscreen = false;
80 |
81 | var canvas = renderer.domElement;
82 | var fullscreenchange = canvas.mozRequestFullScreen ? 'mozfullscreenchange' : 'webkitfullscreenchange';
83 |
84 | document.addEventListener( fullscreenchange, function ( event ) {
85 |
86 | isFullscreen = document.mozFullScreenElement || document.webkitFullscreenElement;
87 |
88 | }, false );
89 |
90 | this.setFullScreen = function ( boolean ) {
91 |
92 | if ( vrHMD === undefined ) return;
93 | if ( isFullscreen === boolean ) return;
94 |
95 | if ( canvas.mozRequestFullScreen ) {
96 |
97 | canvas.mozRequestFullScreen( { vrDisplay: vrHMD } );
98 |
99 | } else if ( canvas.webkitRequestFullscreen ) {
100 |
101 | canvas.webkitRequestFullscreen( { vrDisplay: vrHMD } );
102 |
103 | }
104 |
105 | };
106 |
107 | // render
108 |
109 | var cameraL = new THREE.PerspectiveCamera();
110 | var cameraR = new THREE.PerspectiveCamera();
111 |
112 | this.render = function ( scene, camera ) {
113 |
114 | if ( vrHMD ) {
115 |
116 | var sceneL, sceneR;
117 |
118 | if ( Array.isArray( scene ) ) {
119 |
120 | sceneL = scene[ 0 ];
121 | sceneR = scene[ 1 ];
122 |
123 | } else {
124 |
125 | sceneL = scene;
126 | sceneR = scene;
127 |
128 | }
129 |
130 | var size = renderer.getSize();
131 | size.width /= 2;
132 |
133 | renderer.enableScissorTest( true );
134 | renderer.clear();
135 |
136 | if ( camera.parent === null ) camera.updateMatrixWorld();
137 |
138 | cameraL.projectionMatrix = fovToProjection( eyeFOVL, true, camera.near, camera.far );
139 | cameraR.projectionMatrix = fovToProjection( eyeFOVR, true, camera.near, camera.far );
140 |
141 | camera.matrixWorld.decompose( cameraL.position, cameraL.quaternion, cameraL.scale );
142 | camera.matrixWorld.decompose( cameraR.position, cameraR.quaternion, cameraR.scale );
143 |
144 | cameraL.translateX( eyeTranslationL.x * this.scale );
145 | cameraR.translateX( eyeTranslationR.x * this.scale );
146 |
147 | // render left eye
148 | renderer.setViewport( 0, 0, size.width, size.height );
149 | renderer.setScissor( 0, 0, size.width, size.height );
150 | renderer.render( sceneL, cameraL );
151 |
152 | // render right eye
153 | renderer.setViewport( size.width, 0, size.width, size.height );
154 | renderer.setScissor( size.width, 0, size.width, size.height );
155 | renderer.render( sceneR, cameraR );
156 |
157 | renderer.enableScissorTest( false );
158 |
159 | return;
160 |
161 | }
162 |
163 | // Regular render mode if not HMD
164 |
165 | if ( Array.isArray( scene ) ) scene = scene[ 0 ];
166 |
167 | renderer.render( scene, camera );
168 |
169 | };
170 |
171 | //
172 |
173 | function fovToNDCScaleOffset( fov ) {
174 |
175 | var pxscale = 2.0 / ( fov.leftTan + fov.rightTan );
176 | var pxoffset = ( fov.leftTan - fov.rightTan ) * pxscale * 0.5;
177 | var pyscale = 2.0 / ( fov.upTan + fov.downTan );
178 | var pyoffset = ( fov.upTan - fov.downTan ) * pyscale * 0.5;
179 | return { scale: [ pxscale, pyscale ], offset: [ pxoffset, pyoffset ] };
180 |
181 | }
182 |
183 | function fovPortToProjection( fov, rightHanded, zNear, zFar ) {
184 |
185 | rightHanded = rightHanded === undefined ? true : rightHanded;
186 | zNear = zNear === undefined ? 0.01 : zNear;
187 | zFar = zFar === undefined ? 10000.0 : zFar;
188 |
189 | var handednessScale = rightHanded ? - 1.0 : 1.0;
190 |
191 | // start with an identity matrix
192 | var mobj = new THREE.Matrix4();
193 | var m = mobj.elements;
194 |
195 | // and with scale/offset info for normalized device coords
196 | var scaleAndOffset = fovToNDCScaleOffset( fov );
197 |
198 | // X result, map clip edges to [-w,+w]
199 | m[ 0 * 4 + 0 ] = scaleAndOffset.scale[ 0 ];
200 | m[ 0 * 4 + 1 ] = 0.0;
201 | m[ 0 * 4 + 2 ] = scaleAndOffset.offset[ 0 ] * handednessScale;
202 | m[ 0 * 4 + 3 ] = 0.0;
203 |
204 | // Y result, map clip edges to [-w,+w]
205 | // Y offset is negated because this proj matrix transforms from world coords with Y=up,
206 | // but the NDC scaling has Y=down (thanks D3D?)
207 | m[ 1 * 4 + 0 ] = 0.0;
208 | m[ 1 * 4 + 1 ] = scaleAndOffset.scale[ 1 ];
209 | m[ 1 * 4 + 2 ] = - scaleAndOffset.offset[ 1 ] * handednessScale;
210 | m[ 1 * 4 + 3 ] = 0.0;
211 |
212 | // Z result (up to the app)
213 | m[ 2 * 4 + 0 ] = 0.0;
214 | m[ 2 * 4 + 1 ] = 0.0;
215 | m[ 2 * 4 + 2 ] = zFar / ( zNear - zFar ) * - handednessScale;
216 | m[ 2 * 4 + 3 ] = ( zFar * zNear ) / ( zNear - zFar );
217 |
218 | // W result (= Z in)
219 | m[ 3 * 4 + 0 ] = 0.0;
220 | m[ 3 * 4 + 1 ] = 0.0;
221 | m[ 3 * 4 + 2 ] = handednessScale;
222 | m[ 3 * 4 + 3 ] = 0.0;
223 |
224 | mobj.transpose();
225 |
226 | return mobj;
227 |
228 | }
229 |
230 | function fovToProjection( fov, rightHanded, zNear, zFar ) {
231 |
232 | var DEG2RAD = Math.PI / 180.0;
233 |
234 | var fovPort = {
235 | upTan: Math.tan( fov.upDegrees * DEG2RAD ),
236 | downTan: Math.tan( fov.downDegrees * DEG2RAD ),
237 | leftTan: Math.tan( fov.leftDegrees * DEG2RAD ),
238 | rightTan: Math.tan( fov.rightDegrees * DEG2RAD )
239 | };
240 |
241 | return fovPortToProjection( fovPort, rightHanded, zNear, zFar );
242 |
243 | }
244 |
245 | };
246 |
--------------------------------------------------------------------------------
/vendor/stats.min.js:
--------------------------------------------------------------------------------
1 | // stats.js - http://github.com/mrdoob/stats.js
2 | var Stats=function(){function h(a){c.appendChild(a.dom);return a}function k(a){for(var d=0;de+1E3&&(r.update(1E3*a/(c-e),100),e=c,a=0,t)){var d=performance.memory;t.update(d.usedJSHeapSize/1048576,d.jsHeapSizeLimit/1048576)}return c},update:function(){g=this.end()},domElement:c,setMode:k}};
4 | Stats.Panel=function(h,k,l){var c=Infinity,g=0,e=Math.round,a=e(window.devicePixelRatio||1),r=80*a,f=48*a,t=3*a,u=2*a,d=3*a,m=15*a,n=74*a,p=30*a,q=document.createElement("canvas");q.width=r;q.height=f;q.style.cssText="width:80px;height:48px";var b=q.getContext("2d");b.font="bold "+9*a+"px Helvetica,Arial,sans-serif";b.textBaseline="top";b.fillStyle=l;b.fillRect(0,0,r,f);b.fillStyle=k;b.fillText(h,t,u);b.fillRect(d,m,n,p);b.fillStyle=l;b.globalAlpha=.9;b.fillRect(d,m,n,p);return{dom:q,update:function(f,
5 | v){c=Math.min(c,f);g=Math.max(g,f);b.fillStyle=l;b.globalAlpha=1;b.fillRect(0,0,r,m);b.fillStyle=k;b.fillText(e(f)+" "+h+" ("+e(c)+"-"+e(g)+")",t,u);b.drawImage(q,d+a,m,n-a,p,d,m,n-a,p);b.fillRect(d+n-a,m,a,p);b.fillStyle=l;b.globalAlpha=.9;b.fillRect(d+n-a,m,a,e((1-f/v)*p))}}};"object"===typeof module&&(module.exports=Stats);
6 |
--------------------------------------------------------------------------------
/vendor/tween.min.js:
--------------------------------------------------------------------------------
1 | // tween.js v.0.15.0 https://github.com/sole/tween.js
2 | void 0===Date.now&&(Date.now=function(){return(new Date).valueOf()});var TWEEN=TWEEN||function(){var n=[];return{REVISION:"14",getAll:function(){return n},removeAll:function(){n=[]},add:function(t){n.push(t)},remove:function(t){var r=n.indexOf(t);-1!==r&&n.splice(r,1)},update:function(t){if(0===n.length)return!1;var r=0;for(t=void 0!==t?t:"undefined"!=typeof window&&void 0!==window.performance&&void 0!==window.performance.now?window.performance.now():Date.now();rn;n++)E[n].stop()},this.delay=function(n){return s=n,this},this.repeat=function(n){return e=n,this},this.yoyo=function(n){return a=n,this},this.easing=function(n){return l=n,this},this.interpolation=function(n){return p=n,this},this.chain=function(){return E=arguments,this},this.onStart=function(n){return d=n,this},this.onUpdate=function(n){return I=n,this},this.onComplete=function(n){return w=n,this},this.onStop=function(n){return M=n,this},this.update=function(n){var f;if(h>n)return!0;v===!1&&(null!==d&&d.call(t),v=!0);var M=(n-h)/o;M=M>1?1:M;var O=l(M);for(f in i){var m=r[f]||0,N=i[f];N instanceof Array?t[f]=p(N,O):("string"==typeof N&&(N=m+parseFloat(N,10)),"number"==typeof N&&(t[f]=m+(N-m)*O))}if(null!==I&&I.call(t,O),1==M){if(e>0){isFinite(e)&&e--;for(f in u){if("string"==typeof i[f]&&(u[f]=u[f]+parseFloat(i[f],10)),a){var T=u[f];u[f]=i[f],i[f]=T}r[f]=u[f]}return a&&(c=!c),h=n+s,!0}null!==w&&w.call(t);for(var g=0,W=E.length;W>g;g++)E[g].start(n);return!1}return!0}},TWEEN.Easing={Linear:{None:function(n){return n}},Quadratic:{In:function(n){return n*n},Out:function(n){return n*(2-n)},InOut:function(n){return(n*=2)<1?.5*n*n:-.5*(--n*(n-2)-1)}},Cubic:{In:function(n){return n*n*n},Out:function(n){return--n*n*n+1},InOut:function(n){return(n*=2)<1?.5*n*n*n:.5*((n-=2)*n*n+2)}},Quartic:{In:function(n){return n*n*n*n},Out:function(n){return 1- --n*n*n*n},InOut:function(n){return(n*=2)<1?.5*n*n*n*n:-.5*((n-=2)*n*n*n-2)}},Quintic:{In:function(n){return n*n*n*n*n},Out:function(n){return--n*n*n*n*n+1},InOut:function(n){return(n*=2)<1?.5*n*n*n*n*n:.5*((n-=2)*n*n*n*n+2)}},Sinusoidal:{In:function(n){return 1-Math.cos(n*Math.PI/2)},Out:function(n){return Math.sin(n*Math.PI/2)},InOut:function(n){return.5*(1-Math.cos(Math.PI*n))}},Exponential:{In:function(n){return 0===n?0:Math.pow(1024,n-1)},Out:function(n){return 1===n?1:1-Math.pow(2,-10*n)},InOut:function(n){return 0===n?0:1===n?1:(n*=2)<1?.5*Math.pow(1024,n-1):.5*(-Math.pow(2,-10*(n-1))+2)}},Circular:{In:function(n){return 1-Math.sqrt(1-n*n)},Out:function(n){return Math.sqrt(1- --n*n)},InOut:function(n){return(n*=2)<1?-.5*(Math.sqrt(1-n*n)-1):.5*(Math.sqrt(1-(n-=2)*n)+1)}},Elastic:{In:function(n){var t,r=.1,i=.4;return 0===n?0:1===n?1:(!r||1>r?(r=1,t=i/4):t=i*Math.asin(1/r)/(2*Math.PI),-(r*Math.pow(2,10*(n-=1))*Math.sin(2*(n-t)*Math.PI/i)))},Out:function(n){var t,r=.1,i=.4;return 0===n?0:1===n?1:(!r||1>r?(r=1,t=i/4):t=i*Math.asin(1/r)/(2*Math.PI),r*Math.pow(2,-10*n)*Math.sin(2*(n-t)*Math.PI/i)+1)},InOut:function(n){var t,r=.1,i=.4;return 0===n?0:1===n?1:(!r||1>r?(r=1,t=i/4):t=i*Math.asin(1/r)/(2*Math.PI),(n*=2)<1?-.5*r*Math.pow(2,10*(n-=1))*Math.sin(2*(n-t)*Math.PI/i):r*Math.pow(2,-10*(n-=1))*Math.sin(2*(n-t)*Math.PI/i)*.5+1)}},Back:{In:function(n){var t=1.70158;return n*n*((t+1)*n-t)},Out:function(n){var t=1.70158;return--n*n*((t+1)*n+t)+1},InOut:function(n){var t=2.5949095;return(n*=2)<1?.5*n*n*((t+1)*n-t):.5*((n-=2)*n*((t+1)*n+t)+2)}},Bounce:{In:function(n){return 1-TWEEN.Easing.Bounce.Out(1-n)},Out:function(n){return 1/2.75>n?7.5625*n*n:2/2.75>n?7.5625*(n-=1.5/2.75)*n+.75:2.5/2.75>n?7.5625*(n-=2.25/2.75)*n+.9375:7.5625*(n-=2.625/2.75)*n+.984375},InOut:function(n){return.5>n?.5*TWEEN.Easing.Bounce.In(2*n):.5*TWEEN.Easing.Bounce.Out(2*n-1)+.5}}},TWEEN.Interpolation={Linear:function(n,t){var r=n.length-1,i=r*t,u=Math.floor(i),o=TWEEN.Interpolation.Utils.Linear;return 0>t?o(n[0],n[1],i):t>1?o(n[r],n[r-1],r-i):o(n[u],n[u+1>r?r:u+1],i-u)},Bezier:function(n,t){var r,i=0,u=n.length-1,o=Math.pow,e=TWEEN.Interpolation.Utils.Bernstein;for(r=0;u>=r;r++)i+=o(1-t,u-r)*o(t,r)*n[r]*e(u,r);return i},CatmullRom:function(n,t){var r=n.length-1,i=r*t,u=Math.floor(i),o=TWEEN.Interpolation.Utils.CatmullRom;return n[0]===n[r]?(0>t&&(u=Math.floor(i=r*(1+t))),o(n[(u-1+r)%r],n[u],n[(u+1)%r],n[(u+2)%r],i-u)):0>t?n[0]-(o(n[0],n[0],n[1],n[1],-i)-n[0]):t>1?n[r]-(o(n[r],n[r],n[r-1],n[r-1],i-r)-n[r]):o(n[u?u-1:0],n[u],n[u+1>r?r:u+1],n[u+2>r?r:u+2],i-u)},Utils:{Linear:function(n,t,r){return(t-n)*r+n},Bernstein:function(n,t){var r=TWEEN.Interpolation.Utils.Factorial;return r(n)/r(t)/r(n-t)},Factorial:function(){var n=[1];return function(t){var r,i=1;if(n[t])return n[t];for(r=t;r>1;r--)i*=r;return n[t]=i}}(),CatmullRom:function(n,t,r,i,u){var o=.5*(r-n),e=.5*(i-t),a=u*u,f=u*a;return(2*t-2*r+o+e)*f+(-3*t+3*r-2*o-e)*a+o*u+t}}},"undefined"!=typeof module&&module.exports&&(module.exports=TWEEN);
--------------------------------------------------------------------------------
/vendor/webvr-manager.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 Boris Smus. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 |
17 |
18 | /**
19 | * Helper for getting in and out of VR mode.
20 | * Here we assume VR mode == full screen mode.
21 | *
22 | * 1. Detects whether or not VR mode is possible by feature detecting for
23 | * WebVR (or polyfill).
24 | *
25 | * 2. If WebVR is available, provides means of entering VR mode:
26 | * - Double click
27 | * - Double tap
28 | * - Click "Enter VR" button
29 | *
30 | * 3. Provides best practices while in VR mode.
31 | * - Full screen
32 | * - Wake lock
33 | * - Orientation lock (mobile only)
34 | */
35 | (function() {
36 |
37 | function WebVRManager(renderer, effect, params) {
38 | this.params = params || {};
39 |
40 | // Set option to hide the button.
41 | this.hideButton = this.params.hideButton || false;
42 |
43 | // Save the THREE.js renderer and effect for later.
44 | this.renderer = renderer;
45 | this.effect = effect;
46 |
47 | // Create the button regardless.
48 | this.vrButton = this.createVRButton();
49 |
50 | // Check if the browser is compatible with WebVR.
51 | this.getHMD().then(function(hmd) {
52 | // Activate either VR or Immersive mode.
53 | if (hmd) {
54 | this.activateVR();
55 | } else {
56 | this.activateImmersive();
57 | }
58 | // Set the right mode.
59 | this.defaultMode = hmd ? Modes.COMPATIBLE : Modes.INCOMPATIBLE;
60 | this.setMode(this.defaultMode);
61 | }.bind(this));
62 |
63 | this.os = this.getOS();
64 | this.logo = this.base64('image/png', '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');
65 | this.logoDisabled = this.base64('image/png', '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');
66 | }
67 |
68 | var Modes = {
69 | // Incompatible with WebVR.
70 | INCOMPATIBLE: 1,
71 | // Compatible with WebVR.
72 | COMPATIBLE: 2,
73 | // In virtual reality via WebVR.
74 | VR: 3,
75 | };
76 |
77 | /**
78 | * Promise returns true if there is at least one HMD device available.
79 | */
80 | WebVRManager.prototype.getHMD = function() {
81 | return new Promise(function(resolve, reject) {
82 | navigator.getVRDevices().then(function(devices) {
83 | // Promise succeeds, but check if there are any devices actually.
84 | for (var i = 0; i < devices.length; i++) {
85 | if (devices[i] instanceof HMDVRDevice) {
86 | resolve(devices[i]);
87 | break;
88 | }
89 | }
90 | resolve(null);
91 | }, function() {
92 | // No devices are found.
93 | resolve(null);
94 | });
95 | });
96 | };
97 |
98 | WebVRManager.prototype.isVRMode = function() {
99 | return this.mode == Modes.VR;
100 | };
101 |
102 | WebVRManager.prototype.render = function(scene, camera) {
103 | if (this.isVRMode()) {
104 | this.effect.render(scene, camera);
105 | } else {
106 | this.renderer.render(scene, camera);
107 | }
108 | };
109 |
110 | WebVRManager.prototype.createVRButton = function() {
111 | var button = document.createElement('img');
112 | var s = button.style;
113 | s.position = 'absolute';
114 | s.bottom = '5px';
115 | s.left = 0;
116 | s.right = 0;
117 | s.marginLeft = 'auto';
118 | s.marginRight = 'auto';
119 | s.width = '64px'
120 | s.height = '64px';
121 | s.backgroundSize = 'cover';
122 | s.backgroundColor = 'transparent';
123 | s.border = 0;
124 | s.userSelect = 'none';
125 | s.webkitUserSelect = 'none';
126 | s.MozUserSelect = 'none';
127 | // Prevent button from being dragged.
128 | button.draggable = false;
129 | button.addEventListener('dragstart', function(e) {
130 | e.preventDefault();
131 | });
132 | if (!this.hideButton) document.body.appendChild(button);
133 | return button;
134 | };
135 |
136 | WebVRManager.prototype.setMode = function(mode) {
137 | this.mode = mode;
138 | switch (mode) {
139 | case Modes.INCOMPATIBLE:
140 | this.vrButton.src = this.logo;
141 | this.vrButton.title = 'Open in immersive mode';
142 | break;
143 | case Modes.COMPATIBLE:
144 | this.vrButton.src = this.logo;
145 | this.vrButton.title = 'Open in VR mode';
146 | break;
147 | case Modes.VR:
148 | this.vrButton.src = this.logoDisabled;
149 | this.vrButton.title = 'Leave VR mode';
150 | break;
151 | }
152 |
153 | // Hack for Safari Mac/iOS to force relayout (svg-specific issue)
154 | // http://goo.gl/hjgR6r
155 | this.vrButton.style.display = 'inline-block';
156 | this.vrButton.offsetHeight;
157 | this.vrButton.style.display = 'block';
158 | };
159 |
160 | /**
161 | * Sets the contrast on the button (percent in [0, 1]).
162 | */
163 | WebVRManager.prototype.setContrast = function(percent) {
164 | var value = Math.floor(percent * 100);
165 | this.vrButton.style.webkitFilter = 'contrast(' + value + '%)';
166 | this.vrButton.style.filter = 'contrast(' + value + '%)';
167 | };
168 |
169 | WebVRManager.prototype.base64 = function(format, base64) {
170 | var out = 'data:' + format + ';base64,' + base64;
171 | return out;
172 | };
173 |
174 | /**
175 | * Makes it possible to go into VR mode.
176 | */
177 | WebVRManager.prototype.activateVR = function() {
178 | // Make it possible to enter VR via double click.
179 | window.addEventListener('dblclick', this.enterVR.bind(this));
180 | // Or via double tap.
181 | window.addEventListener('touchend', this.onTouchEnd.bind(this));
182 | // Or via clicking on the VR button.
183 | this.vrButton.addEventListener('mousedown', this.onButtonClick.bind(this));
184 | this.vrButton.addEventListener('touchstart', this.onButtonClick.bind(this));
185 | // Or by hitting the 'f' key.
186 | window.addEventListener('keydown', this.onKeyDown.bind(this));
187 |
188 | // Whenever we enter fullscreen, this is tantamount to entering VR mode.
189 | document.addEventListener('webkitfullscreenchange',
190 | this.onFullscreenChange.bind(this));
191 | document.addEventListener('mozfullscreenchange',
192 | this.onFullscreenChange.bind(this));
193 |
194 | // Create the necessary elements for wake lock to work.
195 | this.setupWakeLock();
196 | };
197 |
198 | WebVRManager.prototype.activateImmersive = function() {
199 | // Next time a user does anything with their mouse, we trigger immersive mode.
200 | this.vrButton.addEventListener('click', this.enterImmersive.bind(this));
201 | };
202 |
203 | WebVRManager.prototype.enterImmersive = function() {
204 | this.requestPointerLock();
205 | this.requestFullscreen();
206 | };
207 |
208 | WebVRManager.prototype.setupWakeLock = function() {
209 | // Create a small video element.
210 | this.wakeLockVideo = document.createElement('video');
211 |
212 | // Loop the video.
213 | this.wakeLockVideo.addEventListener('ended', function(ev) {
214 | this.wakeLockVideo.play();
215 | }.bind(this));
216 |
217 | // Turn on wake lock as soon as the screen is tapped.
218 | triggerWakeLock = function() {
219 | this.requestWakeLock();
220 | }.bind(this);
221 | window.addEventListener('touchstart', triggerWakeLock, false);
222 | };
223 |
224 | WebVRManager.prototype.onTouchEnd = function(e) {
225 | // TODO: Implement better double tap that takes distance into account.
226 | // https://github.com/mckamey/doubleTap.js/blob/master/doubleTap.js
227 |
228 | var now = new Date();
229 | if (now - this.lastTouchTime < 300) {
230 | this.enterVR();
231 | }
232 | this.lastTouchTime = now;
233 | };
234 |
235 | WebVRManager.prototype.onButtonClick = function(e) {
236 | e.stopPropagation();
237 | e.preventDefault();
238 | this.toggleVRMode();
239 | };
240 |
241 | WebVRManager.prototype.onKeyDown = function(e) {
242 | if (e.keyCode == 70) { // 'f'
243 | this.toggleVRMode();
244 | }
245 | };
246 |
247 | WebVRManager.prototype.toggleVRMode = function() {
248 | if (!this.isVRMode()) {
249 | // Enter VR mode.
250 | this.enterVR();
251 | } else {
252 | this.exitVR();
253 | }
254 | };
255 |
256 | WebVRManager.prototype.onFullscreenChange = function(e) {
257 | // If we leave full-screen, also exit VR mode.
258 | if (document.webkitFullscreenElement === null ||
259 | document.mozFullScreenElement === null) {
260 | this.exitVR();
261 | }
262 | };
263 |
264 | /**
265 | * Add cross-browser functionality to keep a mobile device from
266 | * auto-locking.
267 | */
268 | WebVRManager.prototype.requestWakeLock = function() {
269 | this.releaseWakeLock();
270 | if (this.os == 'iOS') {
271 | // If the wake lock timer is already running, stop.
272 | if (this.wakeLockTimer) {
273 | return;
274 | }
275 | this.wakeLockTimer = setInterval(function() {
276 | window.location = window.location;
277 | setTimeout(window.stop, 0);
278 | }, 30000);
279 | } else if (this.os == 'Android') {
280 | // If the video is already playing, do nothing.
281 | if (this.wakeLockVideo.paused === false) {
282 | return;
283 | }
284 | // See videos_src/no-sleep.webm.
285 | this.wakeLockVideo.src = this.base64('video/webm', 'GkXfowEAAAAAAAAfQoaBAUL3gQFC8oEEQvOBCEKChHdlYm1Ch4ECQoWBAhhTgGcBAAAAAAACWxFNm3RALE27i1OrhBVJqWZTrIHfTbuMU6uEFlSua1OsggEuTbuMU6uEHFO7a1OsggI+7AEAAAAAAACkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVSalmAQAAAAAAAEMq17GDD0JATYCMTGF2ZjU2LjQuMTAxV0GMTGF2ZjU2LjQuMTAxc6SQ20Yv/Elws73A/+KfEjM11ESJiEBkwAAAAAAAFlSuawEAAAAAAABHrgEAAAAAAAA+14EBc8WBAZyBACK1nIN1bmSGhVZfVlA4g4EBI+ODhAT3kNXgAQAAAAAAABKwgRC6gRBTwIEBVLCBEFS6gRAfQ7Z1AQAAAAAAALHngQCgAQAAAAAAAFyho4EAAIAQAgCdASoQABAAAEcIhYWIhYSIAgIADA1gAP7/q1CAdaEBAAAAAAAALaYBAAAAAAAAJO6BAaWfEAIAnQEqEAAQAABHCIWFiIWEiAICAAwNYAD+/7r/QKABAAAAAAAAQKGVgQBTALEBAAEQEAAYABhYL/QACAAAdaEBAAAAAAAAH6YBAAAAAAAAFu6BAaWRsQEAARAQABgAGFgv9AAIAAAcU7trAQAAAAAAABG7j7OBALeK94EB8YIBgfCBAw==');
286 | this.wakeLockVideo.play();
287 | }
288 |
289 | }
290 |
291 | /**
292 | * Turn off cross-browser functionality to keep a mobile device from
293 | * auto-locking.
294 | */
295 | WebVRManager.prototype.releaseWakeLock = function() {
296 | if (this.os == 'iOS') {
297 | if (this.wakeLockTimer) {
298 | clearInterval(this.wakeLockTimer);
299 | this.wakeLockTimer = null;
300 | }
301 | } else if (this.os == 'Android') {
302 | this.wakeLockVideo.pause();
303 | this.wakeLockVideo.src = '';
304 | }
305 | };
306 |
307 | WebVRManager.prototype.requestPointerLock = function() {
308 | var canvas = this.renderer.domElement;
309 | canvas.requestPointerLock = canvas.requestPointerLock ||
310 | canvas.mozRequestPointerLock ||
311 | canvas.webkitRequestPointerLock;
312 |
313 | if (canvas.requestPointerLock) {
314 | canvas.requestPointerLock();
315 | }
316 | };
317 |
318 | WebVRManager.prototype.releasePointerLock = function() {
319 | document.exitPointerLock = document.exitPointerLock ||
320 | document.mozExitPointerLock ||
321 | document.webkitExitPointerLock;
322 |
323 | document.exitPointerLock();
324 | };
325 |
326 | WebVRManager.prototype.requestOrientationLock = function() {
327 | if (screen.orientation) {
328 | screen.orientation.lock('landscape');
329 | }
330 | };
331 |
332 | WebVRManager.prototype.releaseOrientationLock = function() {
333 | if (screen.orientation) {
334 | screen.orientation.unlock();
335 | }
336 | };
337 |
338 | WebVRManager.prototype.requestFullscreen = function() {
339 | var canvas = this.renderer.domElement;
340 | if (canvas.mozRequestFullScreen) {
341 | canvas.mozRequestFullScreen();
342 | } else if (canvas.webkitRequestFullscreen) {
343 | canvas.webkitRequestFullscreen();
344 | }
345 | };
346 |
347 | WebVRManager.prototype.releaseFullscreen = function() {
348 | };
349 |
350 | WebVRManager.prototype.getOS = function(osName) {
351 | var userAgent = navigator.userAgent || navigator.vendor || window.opera;
352 | if (userAgent.match(/iPhone/i) || userAgent.match(/iPod/i)) {
353 | return 'iOS';
354 | } else if (userAgent.match(/Android/i)) {
355 | return 'Android';
356 | }
357 | return 'unknown';
358 | };
359 |
360 | WebVRManager.prototype.enterVR = function() {
361 | console.log('Entering VR.');
362 | // Enter fullscreen mode (note: this doesn't work in iOS).
363 | this.effect.setFullScreen(true);
364 | // Lock down orientation, pointer, etc.
365 | this.requestOrientationLock();
366 | // Set style on button.
367 | this.setMode(Modes.VR);
368 | };
369 |
370 | WebVRManager.prototype.exitVR = function() {
371 | console.log('Exiting VR.');
372 | // Leave fullscreen mode (note: this doesn't work in iOS).
373 | this.effect.setFullScreen(false);
374 | // Release orientation, wake, pointer lock.
375 | this.releaseOrientationLock();
376 | this.releaseWakeLock();
377 | // Also, work around a problem in VREffect and resize the window.
378 | this.effect.setSize(window.innerWidth, window.innerHeight);
379 |
380 | // Go back to the default mode.
381 | this.setMode(this.defaultMode);
382 | };
383 |
384 | // Expose the WebVRManager class globally.
385 | window.WebVRManager = WebVRManager;
386 |
387 | })();
--------------------------------------------------------------------------------