Files
Chamilo/app/Resources/public/assets/vrview/src/embed/world-renderer.js
2025-04-10 12:53:50 +02:00

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;