373 lines
11 KiB
JavaScript
373 lines
11 KiB
JavaScript
/*
|
|
* Copyright 2016 Google Inc. All Rights Reserved.
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
var AdaptivePlayer = require('./adaptive-player');
|
|
var EventEmitter = require('eventemitter3');
|
|
var Eyes = require('./eyes');
|
|
var HotspotRenderer = require('./hotspot-renderer');
|
|
var ReticleRenderer = require('./reticle-renderer');
|
|
var SphereRenderer = require('./sphere-renderer');
|
|
var TWEEN = require('@tweenjs/tween.js');
|
|
var Util = require('../util');
|
|
var VideoProxy = require('./video-proxy');
|
|
var WebVRManager = require('webvr-boilerplate');
|
|
|
|
var AUTOPAN_DURATION = 3000;
|
|
var AUTOPAN_ANGLE = 0.4;
|
|
|
|
/**
|
|
* The main WebGL rendering entry point. Manages the scene, camera, VR-related
|
|
* rendering updates. Interacts with the WebVRManager.
|
|
*
|
|
* Coordinates the other renderers: SphereRenderer, HotspotRenderer,
|
|
* ReticleRenderer.
|
|
*
|
|
* Also manages the AdaptivePlayer and VideoProxy.
|
|
*
|
|
* Emits the following events:
|
|
* load: when the scene is loaded.
|
|
* error: if there is an error loading the scene.
|
|
* modechange(Boolean isVR): if the mode (eg. VR, fullscreen, etc) changes.
|
|
*/
|
|
function WorldRenderer(params) {
|
|
this.init_(params.hideFullscreenButton);
|
|
|
|
this.sphereRenderer = new SphereRenderer(this.scene);
|
|
this.hotspotRenderer = new HotspotRenderer(this);
|
|
this.hotspotRenderer.on('focus', this.onHotspotFocus_.bind(this));
|
|
this.hotspotRenderer.on('blur', this.onHotspotBlur_.bind(this));
|
|
this.reticleRenderer = new ReticleRenderer(this.camera);
|
|
|
|
// Get the VR Display as soon as we initialize.
|
|
navigator.getVRDisplays().then(function(displays) {
|
|
if (displays.length > 0) {
|
|
this.vrDisplay = displays[0];
|
|
}
|
|
}.bind(this));
|
|
|
|
}
|
|
WorldRenderer.prototype = new EventEmitter();
|
|
|
|
WorldRenderer.prototype.render = function(time) {
|
|
this.controls.update();
|
|
TWEEN.update(time);
|
|
this.effect.render(this.scene, this.camera);
|
|
this.hotspotRenderer.update(this.camera);
|
|
};
|
|
|
|
/**
|
|
* @return {Promise} When the scene is fully loaded.
|
|
*/
|
|
WorldRenderer.prototype.setScene = function(scene) {
|
|
var self = this;
|
|
var promise = new Promise(function(resolve, reject) {
|
|
self.sceneResolve = resolve;
|
|
self.sceneReject = reject;
|
|
});
|
|
|
|
if (!scene || !scene.isValid()) {
|
|
this.didLoadFail_(scene.errorMessage);
|
|
return;
|
|
}
|
|
|
|
var params = {
|
|
isStereo: scene.isStereo,
|
|
loop: scene.loop,
|
|
volume: scene.volume,
|
|
muted: scene.muted
|
|
};
|
|
|
|
this.setDefaultYaw_(scene.defaultYaw || 0);
|
|
|
|
// Disable VR mode if explicitly disabled, or if we're loading a video on iOS
|
|
// 9 or earlier.
|
|
if (scene.isVROff || (scene.video && Util.isIOS9OrLess())) {
|
|
this.manager.setVRCompatibleOverride(false);
|
|
}
|
|
|
|
// Set various callback overrides in iOS.
|
|
if (Util.isIOS()) {
|
|
this.manager.setFullscreenCallback(function() {
|
|
Util.sendParentMessage({type: 'enter-fullscreen'});
|
|
});
|
|
this.manager.setExitFullscreenCallback(function() {
|
|
Util.sendParentMessage({type: 'exit-fullscreen'});
|
|
});
|
|
this.manager.setVRCallback(function() {
|
|
Util.sendParentMessage({type: 'enter-vr'});
|
|
});
|
|
}
|
|
|
|
// If we're dealing with an image, and not a video.
|
|
if (scene.image && !scene.video) {
|
|
if (scene.preview) {
|
|
// First load the preview.
|
|
this.sphereRenderer.setPhotosphere(scene.preview, params).then(function() {
|
|
// As soon as something is loaded, emit the load event to hide the
|
|
// loading progress bar.
|
|
self.didLoad_();
|
|
// Then load the full resolution image.
|
|
self.sphereRenderer.setPhotosphere(scene.image, params);
|
|
}).catch(self.didLoadFail_.bind(self));
|
|
} else {
|
|
// No preview -- go straight to rendering the full image.
|
|
this.sphereRenderer.setPhotosphere(scene.image, params).then(function() {
|
|
self.didLoad_();
|
|
}).catch(self.didLoadFail_.bind(self));
|
|
}
|
|
} else if (scene.video) {
|
|
if (Util.isIE11()) {
|
|
// On IE 11, if an 'image' param is provided, load it instead of showing
|
|
// an error.
|
|
//
|
|
// TODO(smus): Once video textures are supported, remove this fallback.
|
|
if (scene.image) {
|
|
this.sphereRenderer.setPhotosphere(scene.image, params).then(function() {
|
|
self.didLoad_();
|
|
}).catch(self.didLoadFail_.bind(self));
|
|
} else {
|
|
this.didLoadFail_('Video is not supported on IE11.');
|
|
}
|
|
} else {
|
|
this.player = new AdaptivePlayer(params);
|
|
this.player.on('load', function(videoElement, videoType) {
|
|
self.sphereRenderer.set360Video(videoElement, videoType, params).then(function() {
|
|
self.didLoad_({videoElement: videoElement});
|
|
}).catch(self.didLoadFail_.bind(self));
|
|
});
|
|
this.player.on('error', function(error) {
|
|
self.didLoadFail_('Video load error: ' + error);
|
|
});
|
|
this.player.load(scene.video);
|
|
|
|
this.videoProxy = new VideoProxy(this.player.video);
|
|
}
|
|
}
|
|
|
|
this.sceneInfo = scene;
|
|
if (Util.isDebug()) {
|
|
console.log('Loaded scene', scene);
|
|
}
|
|
|
|
return promise;
|
|
};
|
|
|
|
WorldRenderer.prototype.isVRMode = function() {
|
|
return !!this.vrDisplay && this.vrDisplay.isPresenting;
|
|
};
|
|
|
|
WorldRenderer.prototype.submitFrame = function() {
|
|
if (this.isVRMode()) {
|
|
this.vrDisplay.submitFrame();
|
|
}
|
|
};
|
|
|
|
WorldRenderer.prototype.disposeEye_ = function(eye) {
|
|
if (eye) {
|
|
if (eye.material.map) {
|
|
eye.material.map.dispose();
|
|
}
|
|
eye.material.dispose();
|
|
eye.geometry.dispose();
|
|
}
|
|
};
|
|
|
|
WorldRenderer.prototype.dispose = function() {
|
|
var eyeLeft = this.scene.getObjectByName('eyeLeft');
|
|
this.disposeEye_(eyeLeft);
|
|
var eyeRight = this.scene.getObjectByName('eyeRight');
|
|
this.disposeEye_(eyeRight);
|
|
};
|
|
|
|
WorldRenderer.prototype.destroy = function() {
|
|
if (this.player) {
|
|
this.player.removeAllListeners();
|
|
this.player.destroy();
|
|
this.player = null;
|
|
}
|
|
var photo = this.scene.getObjectByName('photo');
|
|
var eyeLeft = this.scene.getObjectByName('eyeLeft');
|
|
var eyeRight = this.scene.getObjectByName('eyeRight');
|
|
|
|
if (eyeLeft) {
|
|
this.disposeEye_(eyeLeft);
|
|
photo.remove(eyeLeft);
|
|
this.scene.remove(eyeLeft);
|
|
}
|
|
|
|
if (eyeRight) {
|
|
this.disposeEye_(eyeRight);
|
|
photo.remove(eyeRight);
|
|
this.scene.remove(eyeRight);
|
|
}
|
|
};
|
|
|
|
WorldRenderer.prototype.didLoad_ = function(opt_event) {
|
|
var event = opt_event || {};
|
|
this.emit('load', event);
|
|
if (this.sceneResolve) {
|
|
this.sceneResolve();
|
|
}
|
|
};
|
|
|
|
WorldRenderer.prototype.didLoadFail_ = function(message) {
|
|
this.emit('error', message);
|
|
if (this.sceneReject) {
|
|
this.sceneReject(message);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Sets the default yaw.
|
|
* @param {Number} angleRad The yaw in radians.
|
|
*/
|
|
WorldRenderer.prototype.setDefaultYaw_ = function(angleRad) {
|
|
// Rotate the camera parent to take into account the scene's rotation.
|
|
// By default, it should be at the center of the image.
|
|
var display = this.controls.getVRDisplay();
|
|
// For desktop, we subtract the current display Y axis
|
|
var theta = display.theta_ || 0;
|
|
// For devices with orientation we make the current view center
|
|
if (display.poseSensor_) {
|
|
display.poseSensor_.resetPose();
|
|
}
|
|
this.camera.parent.rotation.y = (Math.PI / 2.0) + angleRad - theta;
|
|
};
|
|
|
|
/**
|
|
* Do the initial camera tween to rotate the camera, giving an indication that
|
|
* there is live content there (on desktop only).
|
|
*/
|
|
WorldRenderer.prototype.autopan = function(duration) {
|
|
var targetY = this.camera.parent.rotation.y - AUTOPAN_ANGLE;
|
|
var tween = new TWEEN.Tween(this.camera.parent.rotation)
|
|
.to({y: targetY}, AUTOPAN_DURATION)
|
|
.easing(TWEEN.Easing.Quadratic.Out)
|
|
.start();
|
|
};
|
|
|
|
WorldRenderer.prototype.init_ = function(hideFullscreenButton) {
|
|
var container = document.querySelector('body');
|
|
var aspect = window.innerWidth / window.innerHeight;
|
|
var camera = new THREE.PerspectiveCamera(75, aspect, 0.1, 100);
|
|
camera.layers.enable(1);
|
|
|
|
var cameraDummy = new THREE.Object3D();
|
|
cameraDummy.add(camera);
|
|
|
|
// Antialiasing disabled to improve performance.
|
|
var renderer = new THREE.WebGLRenderer({antialias: false});
|
|
renderer.setClearColor(0x000000, 0);
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
renderer.setPixelRatio(window.devicePixelRatio);
|
|
|
|
container.appendChild(renderer.domElement);
|
|
|
|
var controls = new THREE.VRControls(camera);
|
|
var effect = new THREE.VREffect(renderer);
|
|
|
|
// Disable eye separation.
|
|
effect.scale = 0;
|
|
effect.setSize(window.innerWidth, window.innerHeight);
|
|
|
|
// Present submission of frames automatically. This is done manually in
|
|
// submitFrame().
|
|
effect.autoSubmitFrame = false;
|
|
|
|
this.camera = camera;
|
|
this.renderer = renderer;
|
|
this.effect = effect;
|
|
this.controls = controls;
|
|
this.manager = new WebVRManager(renderer, effect, {predistorted: false, hideButton: hideFullscreenButton});
|
|
|
|
this.scene = this.createScene_();
|
|
this.scene.add(this.camera.parent);
|
|
|
|
|
|
// Watch the resize event.
|
|
window.addEventListener('resize', this.onResize_.bind(this));
|
|
|
|
// Prevent context menu.
|
|
window.addEventListener('contextmenu', this.onContextMenu_.bind(this));
|
|
|
|
window.addEventListener('vrdisplaypresentchange',
|
|
this.onVRDisplayPresentChange_.bind(this));
|
|
};
|
|
|
|
WorldRenderer.prototype.onResize_ = function() {
|
|
this.effect.setSize(window.innerWidth, window.innerHeight);
|
|
this.camera.aspect = window.innerWidth / window.innerHeight;
|
|
this.camera.updateProjectionMatrix();
|
|
};
|
|
|
|
WorldRenderer.prototype.onVRDisplayPresentChange_ = function(e) {
|
|
if (Util.isDebug()) {
|
|
console.log('onVRDisplayPresentChange_');
|
|
}
|
|
var isVR = this.isVRMode();
|
|
|
|
// If the mode changed to VR and there is at least one hotspot, show reticle.
|
|
var isReticleVisible = isVR && this.hotspotRenderer.getCount() > 0;
|
|
this.reticleRenderer.setVisibility(isReticleVisible);
|
|
|
|
// Resize the renderer for good measure.
|
|
this.onResize_();
|
|
|
|
// Analytics.
|
|
if (window.analytics) {
|
|
analytics.logModeChanged(isVR);
|
|
}
|
|
|
|
// When exiting VR mode from iOS, make sure we emit back an exit-fullscreen event.
|
|
if (!isVR && Util.isIOS()) {
|
|
Util.sendParentMessage({type: 'exit-fullscreen'});
|
|
}
|
|
|
|
// Emit a mode change event back to any listeners.
|
|
this.emit('modechange', isVR);
|
|
};
|
|
|
|
WorldRenderer.prototype.createScene_ = function(opt_params) {
|
|
var scene = new THREE.Scene();
|
|
|
|
// Add a group for the photosphere.
|
|
var photoGroup = new THREE.Object3D();
|
|
photoGroup.name = 'photo';
|
|
scene.add(photoGroup);
|
|
|
|
return scene;
|
|
};
|
|
|
|
WorldRenderer.prototype.onHotspotFocus_ = function(id) {
|
|
// Set the default cursor to be a pointer.
|
|
this.setCursor_('pointer');
|
|
};
|
|
|
|
WorldRenderer.prototype.onHotspotBlur_ = function(id) {
|
|
// Reset the default cursor to be the default one.
|
|
this.setCursor_('');
|
|
};
|
|
|
|
WorldRenderer.prototype.setCursor_ = function(cursor) {
|
|
this.renderer.domElement.style.cursor = cursor;
|
|
};
|
|
|
|
WorldRenderer.prototype.onContextMenu_ = function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
return false;
|
|
};
|
|
|
|
module.exports = WorldRenderer;
|