goog.module('gep.components.Exhibition');
const Completer = goog.require('clulib.async.Completer');
const Abstract3DWorld = goog.require('gep.components.Abstract3DWorld');
const {WorldEventType} = goog.require('gep.provider.WorldProvider');
const {ExhibitionProvider,ExhibitionElementOrbitViewModel} = goog.require('gep.provider.ExhibitionProvider');
const {LayerProvider,LayerEventType,LayerEvent,LayerType} = goog.require('gep.provider.LayerProvider');
const VideoProvider = goog.require('gep.provider.VideoProvider');
const SoundProvider = goog.require('gep.provider.SoundProvider');
const AutoResetProvider = goog.require('gep.provider.AutoResetProvider');
const ControlProvider = goog.require('gep.provider.ControlProvider');
const {MoveController,MoveType} = goog.require('gep.controller.MoveController');
const Exhibit = goog.require('gep.world.Exhibit');
const Visitor = goog.require('gep.world.Visitor');
const {HintBox,HintBoxContentType} = goog.require('gep.components.HintBox');
const SecurityProvider = goog.require('gep.provider.SecurityProvider');
const {getBrowserPoints} = goog.require('nbsrc.utils.parser');
const {listen,EventType,Event,listenOnce} = goog.require('goog.events');
const KeyCodes = goog.require('goog.events.KeyCodes');
const classlist = goog.require('goog.dom.classlist');
const dataset = goog.require('goog.dom.dataset');
const {Coordinate} = goog.require('goog.math');
/**
* This component mainly controls the loading, display, control and additional functionality of the entire 3D exhibition.
* @extends {Abstract3DWorld}
*/
class Exhibition extends Abstract3DWorld
{
constructor()
{
super();
/**
* Reference to SecurityProvider for monitoring the data security consent.
* @type {SecurityProvider}
* @private
*/
this.securityProvider_ = SecurityProvider.getInstance();
/**
* Reference to ExhibitionProvider to communicate with the backend and provide and monitor all information that dynamically changes the application.
* @type {ExhibitionProvider}
* @private
*/
this.exhibitionProvider_ = ExhibitionProvider.getInstance();
/**
* Reference to LayerProvider for monitoring and controlling layer appearance and disappearance.
* @type {LayerProvider}
* @private
*/
this.layerProvider_ = LayerProvider.getInstance();
/**
* Reference to VideoProvider for creating and monitoring Vimeo video instances.
* @type {VideoProvider}
* @private
*/
this.videoProvider_ = VideoProvider.getInstance();
/**
* Reference to AutoResetProvider to monitor and control the automatic reset of the page when the user is inactive for a specified period of time.
* @type {AutoResetProvider}
* @private
*/
this.autoResetProvider_ = AutoResetProvider.getInstance();
/**
* Reference to SoundProvider for monitoring and executing sound loading processes and sound playback controling.
* @type {SoundProvider}
* @private
*/
this.soundProvider_ = SoundProvider.getInstance();
/**
* Reference to ControlProvider to monitor the activation of a gamepad controller so that the application's control is adapted to it.
* @type {ControlProvider}
* @private
*/
this.controlProvider_ = ControlProvider.getInstance();
/**
* Initialization of the MoveController with values for acceleration and friction.
* `x`, `z`: values for the visitor movement in the in the 3D space;
* `rh`, `rv`: values visitor rotation in the 3D space
* @type {MoveController}
* @private
*/
this.moveController_ = new MoveController(
new Map([
['x', this.worldProvider_.getMoveSettings('accelerationMoveSide')],
['y', 0],
['z', this.worldProvider_.getMoveSettings('accelerationMoveDirect')],
['rh', this.worldProvider_.getMoveSettings('accelerationRotateHorizontal')],
['rv', this.worldProvider_.getMoveSettings('accelerationRotateVertical')]]),
new Map([
['x', this.worldProvider_.getMoveSettings('frictionMoveSide')],
['y', 0],
['z', this.worldProvider_.getMoveSettings('frictionMoveDirect')],
['rh', this.worldProvider_.getMoveSettings('frictionRotateHorizontal')],
['rv', this.worldProvider_.getMoveSettings('frictionRotateVertical')]]),
);
this.moveController_.velocity = {'x': 0, 'y': 0, 'z': 0, 'rh': 0, 'rv': 0};
/**
* Main THREE scene
* @type {THREE.Scene}
* @private
*/
this.scene_ = new THREE.Scene();
/**
* List of created lights for the 3D scene
* @type {Map<string,THREE.Light>}
* @private
*/
this.lights_ = new Map();
/**
* list of all created exhibits
* @type {Array<Exhibit>}
* @private
*/
this.exhibits_ = [];
/**
* Cannon world for handling all Cannon physic objects.
* @type {CANNON.World}
* @private
*/
this.physicWorld_ = null;
/**
* Instance of the created visitor object
* @type {Visitor}
* @private
*/
this.visitor_ = null;
/**
* 3D container that contains the camera for the top view and is used for position changes.
* @type {THREE.Group}
* @protected
*/
this.cameraTopviewWrapper_ = new THREE.Group();
/**
* Camera for rendering the top view
* @type {THREE.Camera}
* @protected
*/
this.cameraTopview_ = null;
/**
* Camera vor randering an exhibit orbit view
* @type {THREE.Camera}
* @protected
*/
this.cameraOribitview_ = null;
/**
* Ground color plane
* @type {THREE.Mesh}
* @private
*/
this.ground_ = null;
/**
* List of Cannon sensors that have detected a collision change in {@Link Exhibition#handleCollisionStart_} and {@Link Exhibition#handleCollisionEnd_}
* @type {Array<string>}
* @private
*/
this.collidedSensors_ = [];
/**
* Timeout id for the delayed appearing of view switch buttons
* @type {number}
* @private
*/
this.viewSwitchTimeout_ = 0;
/**
* THREE orbit controls will be used for the exhibit orbit view
* @type {THREE.OrbitControls}
* @protected
*/
this.orbitControls_ = null;
/**
* Holds the current orbit view configuration (azimuthal angle, polar angle and zoom)
* @type {{azimuthalAngle:number,polarAngle:number,zoom:number}}
* @private
*/
this.orbit_ = {azimuthalAngle:0, polarAngle:0, zoom:0};
/**
* Any values can be stored in this object. The zoom value is stored before the change from the visitor view to
* the exhibit orbit view in order to animate back to this value during the animation in the initial state.
* @type {Map<string,number>}
* @private
*/
this.savedProperties_ = new Map();
/**
* Contains all THREE collision objects of the exhibits for checking the collision by raycasting.
* @type {Array<THREE.Object3D>}
* @private
*/
this.allExhibitColliders_ = [];
/**
* This value indicates that it is the first entering of an activation zone by a visitor after reshuffling.
* As a result, no interactions with the activation zones are triggered during reshuffling.
* @type {boolean}
* @private
*/
this.isFirstActivation_ = true;
/**
* Specifies when to check in exhibits whether certain 3D elements can be removed after the first interaction with an artwork.
* @type {boolean}
* @private
*/
this.checkExhibits_ = false;
/**
* Cannon physics debugger to display polygon outline segments for the mass collision objects when reshuffeling occurs.
* @type {Object}
* @private
*/
this.physicDebugger_ = null;
/**
* An optional countdown for the start of reshuffling can be displayed here. The countdown is then displayed at the bottom right of the page.
* @type {number}
* @private
*/
this.activationCountdown_ = 0;
/**
* Holds the values of a detected Cannon sensor that was activated during the reshuffeling and is executed if it is still active when the reshuffeling is complete.
* @type {{elementId:number, exhibitId:number}|null}
* @private
*/
this.virtualActiveSensor_ = null;
/**
* Specifies whether debug rendering is generally enabled for the Cannon world. This can be activated via the URL parameter `physic=1`.
* @type {boolean}
* @private
*/
this.enableDebugRendering_ = window['PHYSIC_MODE'] === 'on';
/**
* Defines if the countdown for the reshuffeling of the exhibits is running.
* @type {boolean}
* @private
*/
this.countdownIsRunning_ = false;
/**
* Promise which is solved when the navigation hint boxes (`NAVIGATION_MOVEMENT`, `NAVIGATION_PERSPECTIVE` and `NAVIGATION_TOPVIEW`) were displayed.
* @type {Promise}
* @private
*/
this.hintNavigationPromise_ = null;
/**
* Completer is a helper which returns a promise. It is solved when the first interaction of the user has been registered.
* In this case, the user has clicked on the audio hint box button that is displayed at startup.
* @type {Completer}
* @private
*/
this.firstInteractionCompleter_ = new Completer();
/**
* Represents the Helper GUI {@Link https://github.com/dataarts/dat.gui} to change some world settings {@Link WorldProvider#settings} on the fly.
* The GUI is activated and displayed by the URL parameter `gui=1`.
* @type {dat.GUI}
* @private
*/
this.gui_ = null;
}
/**
* Component is ready and had loaded all dependencies (inherit method waitFor and sub components).
* @inheritDoc
*/
onInit()
{
this.shouldBuildImmediately_ = false;
classlist.enable(document.body, 'is-mobile-device', this.worldProvider_.isMobileDevice);
super.onInit();
/**
* Dom reference for the button which should show the exhibit infos.
* @type {Element}
* @private
*/
this.exhibitInfoOpener_ = this.getElement().querySelector('.exhibit-info-opener');
/**
* Dom reference for the button which should switch into the exhibit/orbit view.
* @type {Element}
* @private
*/
this.exhibitViewOpener_ = this.getElement().querySelector('.exhibit-view-opener--orbit');
/**
* Dom reference for the button which should switch back to the normal visitor view.
* @type {Element}
* @private
*/
this.exhibitViewCloser_ = this.getElement().querySelector('.exhibit-view-closer');
/**
* Dom reference for the button which should show an external installation in an iframe layer.
* @type {Element}
* @private
*/
this.exhibitFrameOpener_ = this.getElement().querySelector('.exhibit-view-opener--frame');
/**
* Dom reference for the interaction hint which is displayed until the reshuffeling is completed.
* @type {Element}
* @private
*/
this.exhibitInteractiveHint_ = this.getElement().querySelector('.exhibit-hint--interactive');
/**
* Component reference for controlling the hint appearing.
* @type {HintBox}
* @private
*/
this.hintBox_ = /** @type {HintBox} */ (this.queryComponent('[data-cmp="hint-box"]'));
/**
* Dom reference for the activation hint when a {@Link Exhibition#activationCountdown_} time is defined.
* @type {Element}
* @private
*/
this.activationHint_ = /** @type {Element} */ (document.body.querySelector('.activation-hint'));
/** @type {!Element} */ (this.activationHint_.querySelector('.activation-hint-counter')).innerHTML = this.activationCountdown_.toString();
/**
* Interval id for checking the loading progress.
* @type {number}
* @private
*/
this.loadCheckInterval_ = 0;
/**
* Defines if the mass objects of all exhibits are scalled up for normal collision behaviour.
* @type {boolean}
* @private
*/
this.allMassBodiesAreScalledUp_ = false;
this.hintBox_.show(HintBoxContentType.ACTIVATE_AUDIO, 0).then(() => {
// check the user data securioty consent
if (window['GAMEPAD_MODE'] !== 'on' && this.securityProvider_.legalsAccepted === false)
{
this.layerProvider_.show('blocking');
listenOnce(this.securityProvider_, EventType.CHANGE, () => {
this.firstInteractionCompleter_.resolve();
}, false, this);
}
else
{
this.firstInteractionCompleter_.resolve();
}
}, (error) => {console.warn(error);});
this.initScene_().then(() => {
this.creationCompleter_.resolve();
});
this.ready().then(() => {
this.soundProvider_.enableBackgroundSound = true;
this.renderScene_(true);
classlist.add(this.getElement(), 'is-ready');
this.activateScene_();
this.countdownIsRunning_ = true;
if(this.activationCountdown_ > 0)
setTimeout(() => { classlist.enable(this.activationHint_, 'is-visible', true); }, 1000);
else
this.hintBox_.show(HintBoxContentType.ACTIVATE_RESHUFFLING, 0).catch((error) => {console.warn(error);});
});
let profileOpener = this.activationHint_.querySelector('.activation-hint-message-link--profile');
if(profileOpener)
listen(profileOpener, EventType.CLICK, () => {
this.layerProvider_.show('visitor', {'id': 'profile'});
}, false, this);
listen(this.controlProvider_, EventType.UPDATE, this.checkControlling_, false, this);
//blocks browser scroll with arrow keys
listen(window, EventType.KEYDOWN, (event) => { if([KeyCodes.UP, KeyCodes.DOWN].indexOf(event.keyCode) != -1) event.preventDefault(); }, false);
listen(document.documentElement, EventType.KEYDOWN, (event) => {
if(window['GAMEPAD_MODE'] === 'on' && [KeyCodes.R, KeyCodes.F, KeyCodes.V, KeyCodes.G, KeyCodes.X, KeyCodes.Y, KeyCodes.Z, KeyCodes.B].indexOf(event.keyCode) != -1) {
this.layerProvider_.isOpen('guiding') ? this.layerProvider_.hide('guiding') : this.layerProvider_.show('guiding');
}
}, false, this);
}
/**
* Checks and updates the controller settings.
* @private
*/
checkControlling_()
{
if(this.visitor_)
{
this.visitor_.cameraPolarAngleRange.start = this.worldProvider_.getMoveSettings('verticalRotationLimitDown') * Math.PI / 180;
this.visitor_.cameraPolarAngleRange.end = this.worldProvider_.getMoveSettings('verticalRotationLimitUp') * Math.PI / 180;
}
this.moveController_.setAcceleration(this.worldProvider_.getMoveSettings('accelerationMoveSide'),'x');
this.moveController_.setAcceleration(this.worldProvider_.getMoveSettings('accelerationMoveDirect'),'z');
this.moveController_.setAcceleration(this.worldProvider_.getMoveSettings('accelerationRotateHorizontal'),'rh');
this.moveController_.setAcceleration(this.worldProvider_.getMoveSettings('accelerationRotateVertical'),'rv');
this.moveController_.setFriction(this.worldProvider_.getMoveSettings('frictionMoveSide'), 'x');
this.moveController_.setFriction(this.worldProvider_.getMoveSettings('frictionMoveDirect'), 'z');
this.moveController_.setFriction(this.worldProvider_.getMoveSettings('frictionRotateHorizontal'), 'rh');
this.moveController_.setFriction(this.worldProvider_.getMoveSettings('frictionRotateVertical'), 'rv');
}
/**
* Can be called to receive a Promise that is triggered when the main exhibition is loaded and the first interaction of the user was fulfilled.
* @inheritDoc
*/
ready()
{
return Promise.all([
super.ready(),
this.firstInteractionCompleter_.getPromise().then(() => { classlist.add(document.documentElement, 'has-completed-first-interaction'); })
]);
}
/**
* Creates the main WebGLRenderer and updates some of its settings.
* @inheritDoc
*/
createRenderer_()
{
super.createRenderer_();
if(this.renderer_)
{
this.renderer_.outputEncoding = THREE.sRGBEncoding;
this.renderer_.toneMapping = THREE.ACESFilmicToneMapping;
}
}
/**
* Loads and creates the 3D scene.
* @inheritDoc
*/
async buildScene_()
{
// Define ids of exhibits which should be excluded in mobile or desktop version
this.exhibitionProvider_.excludedItemIds = this.worldProvider_.isMobileDevice ? [] : [];
await this.exhibitionProvider_.ready();
const visitorTopTexture = await this.exhibitionProvider_.loadTextureFile('images/textures/visitor-sphere.png').then((texture) => {return texture;});
this.visitor_ = new Visitor();
this.visitor_.build(new THREE.Vector3(0, this.visitor_.cameraNormalY, 20), .6, visitorTopTexture);
this.visitor_.object3D.rotation.y = 0 * Math.PI / 180;
this.scene_.add(this.visitor_.object3D);
this.cameraTopview_ = new THREE.PerspectiveCamera(30.0, window.innerWidth / window.innerHeight, 1, 1000); // 35.0, window.innerWidth / window.innerHeight, .1, 2000
this.cameraTopviewWrapper_.add(this.cameraTopview_);
this.cameraTopview_.name = "topviewCamera";
this.scene_.add(this.cameraTopviewWrapper_);
this.scene_.background = new THREE.Color(this.worldProvider_.settings.background['color']);
this.scene_.fog = new THREE.FogExp2(this.worldProvider_.settings.fog['color'], this.worldProvider_.settings.fog['density']);
let groundMaterial = new THREE.MeshPhongMaterial({'color': this.worldProvider_.settings.ground['color']});
this.ground_ = new THREE.Mesh(
new THREE.PlaneGeometry(2000, 2000),
groundMaterial
);
this.ground_.name = 'ground';
this.ground_.position.set(0, 0, 0);
this.ground_.rotation.x = -90 * (Math.PI / 180);
this.ground_.castShadow = false;
this.ground_.receiveShadow = true;
this.scene_.add(this.ground_);
this.worldProvider_.intersectables.push(this.ground_);
this.changeCamera_(this.visitor_.camera);
let hemisphereLight = new THREE.HemisphereLight(
this.worldProvider_.settings.lights['hemiLightColor'],
this.worldProvider_.settings.lights['hemiLightGroundColor'],
this.worldProvider_.settings.lights['hemiLightIntensity']
);
hemisphereLight.position.set(0, 0, 0);
this.scene_.add(hemisphereLight);
this.lights_.set('hemisphereLight', hemisphereLight);
if(window['GUI_MODE'] === 'on')
this.createDebugGui_();
//physic
this.physicWorld_ = new CANNON.World({
'gravity': new CANNON.Vec3(0, 0, 0)
});
this.physicWorld_.solver.iterations = 10;
this.physicWorld_.addBody(this.visitor_.physicBody);
this.exhibitionProvider_.physicBodyReferences.set(this.visitor_.physicBody.id, 'visitor::mass');
this.physicWorld_.addBody(this.visitor_.physicSensor);
this.exhibitionProvider_.physicBodyReferences.set(this.visitor_.physicSensor.id, 'visitor');
//loading
let soundProgress = 0;
this.soundProvider_.soundAssetPath = 'sounds/';
this.soundProvider_.load(['background.mp3']).then(() => { soundProgress = 1; }, (error) => { console.warn(error); });
let totalProgress = 1; // 2 for reflection texture and sound
let cubeTextureProgress = 0;
this.exhibitionProvider_.items.forEach((item) => {
if(item.transformFile.trim() != '')
totalProgress++;
item.elements.forEach((element) => {
if(element.file3D.trim() != '')
totalProgress++;
});
});
// Interval checking for the loading progress
this.loadCheckInterval_ = setInterval(() => {
let progress = cubeTextureProgress + soundProgress;
this.exhibits_.forEach((item) => {
progress += item.progress;
});
let loadProgress = 1 / totalProgress * progress;
this.worldProvider_.updateLoadProgress(loadProgress);
if(loadProgress == 1)
{
clearTimeout(this.loadCheckInterval_);
}
}, 100);
//reflection texture
await this.exhibitionProvider_.loadReflectionTexture((progress) => {cubeTextureProgress = progress;}).then(() => {cubeTextureProgress = 1;});
//exhibits
const massObjects = [];
for(let i=0; i<this.exhibitionProvider_.items.length; i++)
{
let exhibit = new Exhibit(i, this.exhibitionProvider_.items[i]);
this.exhibits_.push(exhibit);
await exhibit.build();
exhibit.updateMassBodyScale(.001);
this.scene_.add(exhibit.object3D);
exhibit.physicBodies.forEach((physicBody) => {
this.physicWorld_.addBody(physicBody);
});
exhibit.physicColliders.forEach((physicBody) => {
this.physicWorld_.addBody(physicBody);
});
this.allExhibitColliders_ = this.allExhibitColliders_.concat(exhibit.collisionObjects);
massObjects.push(exhibit.physicMassBody);
}
this.physicDebugger_ = CANNON.cannonDebugger(this.scene_, window['PHYSIC_MODE'] === 'on' ? this.physicWorld_.bodies : massObjects, {autoUpdate: false});
this.worldProvider_.loaded();
}
/**
* Defines the values that can be shown and changed in the GUI. The GUI is displayed with the url parameter `gui=1`.
* @private
*/
createDebugGui_()
{
if (this.gui_)
return;
this.gui_ = new dat.GUI({'width': 400});
// background
const backgroundGUI = this.gui_.addFolder('background');
backgroundGUI.addColor(this.worldProvider_.settings.background, 'color')
.onChange(() => {
this.scene_.background.set(new THREE.Color(this.worldProvider_.settings.background['color']));
});
// lighting
const lightingGUI = this.gui_.addFolder('lighting');
lightingGUI.addColor(this.worldProvider_.settings.lights, 'hemiLightColor')
.onChange(() => {
this.lights_.get('hemisphereLight').color.set(this.worldProvider_.settings.lights['hemiLightColor']);
});
lightingGUI.addColor(this.worldProvider_.settings.lights, 'hemiLightGroundColor')
.onChange(() => {
/** @type {THREE.HemisphereLight} */ (this.lights_.get('hemisphereLight')).groundColor.set(this.worldProvider_.settings.lights['hemiLightGroundColor']);
});
lightingGUI.add(this.worldProvider_.settings.lights, 'hemiLightIntensity', 0, 1).step(0.01).listen()
.onChange(() => {
this.lights_.get('hemisphereLight').intensity = this.worldProvider_.settings.lights['hemiLightIntensity'];
});
// scene & color
const fogGUI = this.gui_.addFolder('fog');
fogGUI.addColor(this.worldProvider_.settings.fog, 'color')
.onChange(() => {
this.scene_.fog.color.set(this.worldProvider_.settings.fog['color']);
});
fogGUI.add(this.worldProvider_.settings.fog, 'density', 0, .1).step(0.001).listen()
.onChange(() => {
this.scene_.fog.density = this.worldProvider_.settings.fog['density'];
});
const groundGUI = this.gui_.addFolder('ground');
groundGUI.addColor(this.worldProvider_.settings.ground, 'color')
.onChange(() => {
this.ground_.material.color.set(this.worldProvider_.settings.ground['color']);
});
const artworkGUI = this.gui_.addFolder('artwork');
artworkGUI.add(this.worldProvider_.settings.artwork, 'envMap')
.onChange(() => {
this.exhibits_.forEach((exhibit) => {
exhibit.enableMaterialEnvMap(this.worldProvider_.settings.artwork['envMap']);
})
});
// movment
const controllsGUI = this.gui_.addFolder('controlls');
controllsGUI.add(this.worldProvider_.settings.moving, 'accelerationMoveDirect', 0, 200).step(1).listen()
.onChange(() => {
this.moveController_.setAcceleration(this.worldProvider_.settings.moving['accelerationMoveDirect'], 'z');
});
controllsGUI.add(this.worldProvider_.settings.moving, 'frictionMoveDirect', 0, 20).step(1).listen()
.onChange(() => {
this.moveController_.setFriction(this.worldProvider_.settings.moving['frictionMoveDirect'], 'z');
});
controllsGUI.add(this.worldProvider_.settings.moving, 'accelerationMoveSide', 0, 200).step(1).listen()
.onChange(() => {
this.moveController_.setAcceleration(this.worldProvider_.settings.moving['accelerationMoveSide'], 'x');
});
controllsGUI.add(this.worldProvider_.settings.moving, 'frictionMoveSide', 0, 20).step(1).listen()
.onChange(() => {
this.moveController_.setFriction(this.worldProvider_.settings.moving['frictionMoveSide'], 'x');
});
controllsGUI.add(this.worldProvider_.settings.moving, 'accelerationRotateHorizontal', 0, 100).step(1).listen()
.onChange(() => {
this.moveController_.setAcceleration(this.worldProvider_.settings.moving['accelerationRotateHorizontal'], 'rh');
});
controllsGUI.add(this.worldProvider_.settings.moving, 'frictionRotateHorizontal', 0, 20).step(1).listen()
.onChange(() => {
this.moveController_.setFriction(this.worldProvider_.settings.moving['frictionRotateHorizontal'], 'rh');
});
controllsGUI.add(this.worldProvider_.settings.moving, 'accelerationRotateVertical', 0, 100).step(1).listen()
.onChange(() => {
this.moveController_.setAcceleration(this.worldProvider_.settings.moving['accelerationRotateVertical'], 'rv');
});
controllsGUI.add(this.worldProvider_.settings.moving, 'frictionRotateVertical', 0, 20).step(1).listen()
.onChange(() => {
this.moveController_.setFriction(this.worldProvider_.settings.moving['frictionRotateVertical'], 'rv');
});
}
/**
* Enable the active scene for rendering and interaction.
* Adds all important listener.
* @inheritDoc
*/
activateScene_()
{
super.activateScene_();
this.worldProvider_.activate();
this.autoResetProvider_.start();
this.moveController_.activate(/** @type {!Element} */ (this.getElement()), false, false, false, window['GAMEPAD_MODE'] === 'on');
listen(this.worldProvider_, WorldEventType.SWITCH_WORLD_VIEW, this.handleSwitchView_, false, this);
listen(this.worldProvider_, WorldEventType.SWITCH_EXHIBIT_VIEW, this.handleSwitchExhibitView_, false, this);
listen(this.exhibitInfoOpener_, EventType.CLICK, this.openExhibitInfo_, false, this);
listen(this.exhibitViewOpener_, EventType.CLICK, this.changeExhibitView_, false, this);
listen(this.exhibitViewCloser_, EventType.CLICK, this.changeExhibitView_, false, this);
listen(this.exhibitFrameOpener_, EventType.CLICK, this.showExhibitFrame_, false, this);
listen(this.layerProvider_, LayerEventType.SHOW_LAYER, this.handleLayerShow_, false, this);
listen(this.layerProvider_, LayerEventType.HIDE_LAYER, this.handleLayerHide_, false, this);
listen(this.videoProvider_, EventType.PLAY, this.handleVideoPlay_, false, this);
listen(this.videoProvider_, EventType.PAUSE, this.handleVideoPause_, false, this);
listen(this.renderer_.domElement, EventType.CLICK, this.handleCanvasClick_, false, this);
this.visitor_.physicSensor.addEventListener('collide', this.handleCollisionStart_.bind(this));
this.physicWorld_.addEventListener('endContact', this.handleCollisionEnd_.bind(this));
this.physicWorld_.addEventListener("preStep", this.handlePhysicPreStep_.bind(this));
this.worldProvider_.physicIsEnabled = true;
}
/**
* Shows the different navigation hints ({@Link HintBox}) for the user one after the other.
* @return {Promise}
* @private
*/
showNavigationHints_()
{
if(window['GAMEPAD_MODE'] == 'on')
return Promise.resolve();
if(!this.hintNavigationPromise_) {
this.hintNavigationPromise_ = this.hintBox_.show(HintBoxContentType.NAVIGATION_MOVEMENT).then(() => {
return this.hintBox_.show(HintBoxContentType.NAVIGATION_PERSPECTIVE).then(() => {
if(!this.worldProvider_.isMobileDevice)
return this.hintBox_.show(HintBoxContentType.NAVIGATION_TOPVIEW).catch((error) => {console.warn(error);});
}, (error) => {console.warn(error);});
}, (error) => {console.warn(error);});
}
return this.hintNavigationPromise_;
}
/**
* Initializes the opening of the layer for the exhibit infos.
* @private
*/
openExhibitInfo_()
{
if(this.resizeProvider_.getViewportSize().width < 1024)
this.layerProvider_.hide('visitor');
this.layerProvider_.show('exhibit', {'id': this.worldProvider_.activeExhibit.id, 'elementId': this.worldProvider_.activeExhibit.elementId});
}
/**
* Analyzes the click on the canvas when the exhibition is in top view and checks by raycasting
* if an object in the list of {@Link Worldprovider#intersectables} has been hit.
* The position of the visitor changes to the clicked position.
* A minimum distance to the next artwork is kept so that the user does not move too far away from all objects in the room.
* If an intersectable (exhibit or scenographic element) is hit, the next possible free position to the object is calculated.
* @param {Event} event
* @private
*/
handleCanvasClick_(event)
{
// topview
if(this.worldProvider_.isTopView && !this.worldProvider_.isSwitchingView)
{
let point = getBrowserPoints(event)[0];
let viewSize = this.resizeProvider_.getViewportSize();
let mouseVector = new THREE.Vector2(
(point.x / viewSize.width) * 2 - 1,
-(point.y / viewSize.height) * 2 + 1
);
this.raycaster_.setFromCamera(mouseVector, this.camera_);
this.raycaster_.far = Infinity;
let intersects = this.raycaster_.intersectObjects(this.worldProvider_.intersectables);
if(intersects[0])
{
let pos = null;
if(intersects[0]['object']['name'] == 'ground')
{
pos = /** @type {THREE.Vector3} */ (intersects[0]['point']);
}
else if(intersects[0]['object']['name'].substr(0,14) == 'boundingObject')
{
let nameObj = intersects[0]['object']['name'].split('::');
let exhibit = this.exhibits_[parseInt(nameObj[1], 10)];
let elementId = nameObj[2] != '' ? parseInt(nameObj[2], 10) : exhibit.model.artworks.length == 1 ? exhibit.model.artworks[0] : '';
if(exhibit && elementId)
pos = exhibit.getWorldPositionOf('interactionObject::'+exhibit.id.toString()+'::'+elementId.toString());
}
if(pos != null)
{
let closestExhibit = null;
let distanceVisitorToClosestExhibit = Infinity;
this.exhibits_.forEach((exhibit) => {
let distance = /** @type {THREE.Vector3} */ (pos).distanceTo(exhibit.object3D.position);
if(distance < distanceVisitorToClosestExhibit)
{
closestExhibit = exhibit;
distanceVisitorToClosestExhibit = distance;
}
});
// check if new pos is outside the visitor max distance to any exhibit
if(closestExhibit && distanceVisitorToClosestExhibit > this.visitor_.maxDistanceToExhibit)
{
const angle = closestExhibit.object3D.position.angleTo(pos);
pos.x = Math.cos(angle) * this.visitor_.maxDistanceToExhibit + closestExhibit.object3D.position.x;
pos.z = Math.sin(angle) * this.visitor_.maxDistanceToExhibit + closestExhibit.object3D.position.z;
}
let distance = Math.sqrt(Math.pow(this.cameraTopviewWrapper_.position.x - pos.x,2) + Math.pow(this.cameraTopviewWrapper_.position.z - pos.z,2));
gsap.to(this.cameraTopviewWrapper_.position, {'x': pos.x, 'z': pos.z, 'duration': Math.min(1, distance), 'ease': 'power2.inOut', 'onComplete': () => {
this.worldProvider_.switchWorldView();
}});
}
}
}
}
/**
* Performs the animated switch from visitor view to top view and vice versa.
* @private
*/
handleSwitchView_()
{
this.worldProvider_.isSwitchingView = true;
this.autoResetProvider_.start();
let pos = this.visitor_.camera.getWorldPosition(new THREE.Vector3());
this.visitor_.enableTopViewPlane(this.worldProvider_.isTopView, this.worldProvider_.isTopView ? .6 : 0);
// switches from visitor view to top view
if(this.worldProvider_.isTopView)
{
const rotateToGlobaleAxis = false; //set to true to have a correct compass view
gsap.killTweensOf(this.cameraTopviewWrapper_.position);
let tweenObject = {
'wy': pos.y,
'wry': this.visitor_.object3D.rotation.y,
'fd': this.scene_.fog.density,
'crx': 0,
'hli': this.lights_.get('hemisphereLight').intensity,
};
let targetRotation = rotateToGlobaleAxis ? 0 : tweenObject['wry'];
this.cameraTopviewWrapper_.position.set(pos.x, pos.y, pos.z);
this.cameraTopviewWrapper_.rotation.y = tweenObject['wry'];
this.changeCamera_(this.cameraTopview_);
let timeline = gsap.timeline({'onComplete': () => {
this.worldProvider_.isSwitchingView = false;
}});
timeline.to(tweenObject, {'wy': 300, 'wry': targetRotation, 'fd': 0, 'hli': this.worldProvider_.settings.lights['hemiLightIntensityTopview'], 'duration': 1, 'ease': 'power2.inOut',
'onUpdate': () => {
this.cameraTopviewWrapper_.position.y = tweenObject['wy'];
this.cameraTopviewWrapper_.rotation.y = tweenObject['wry'];
this.scene_.fog.density = tweenObject['fd'];
this.lights_.get('hemisphereLight').intensity = tweenObject['hli'];
}
}, 0);
timeline.to(tweenObject, {'crx': -90 * Math.PI / 180, 'duration': .75, 'ease': 'power2.inOut',
'onUpdate': () => {
this.cameraTopview_.rotation.x = tweenObject['crx'];
}
}, 0);
}
// switches from top view to visitor view
else
{
let tweenObject = {
'wy': this.cameraTopviewWrapper_.position.y,
'wry': this.cameraTopviewWrapper_.rotation.y,
'fd': this.scene_.fog.density,
'crx': this.cameraTopview_.rotation.x,
'hli': this.lights_.get('hemisphereLight').intensity,
};
let timeline = gsap.timeline({'onComplete': () => {
this.worldProvider_.isSwitchingView = false;
this.visitor_.physicBody.position.x = this.cameraTopviewWrapper_.position.x;
this.visitor_.physicBody.position.z = this.cameraTopviewWrapper_.position.z;
this.changeCamera_(this.visitor_.camera);
}});
timeline.to(tweenObject, {'wy': pos.y, 'wry': this.visitor_.object3D.rotation.y, 'fd': this.worldProvider_.settings.fog['density'], 'hli': this.worldProvider_.settings.lights['hemiLightIntensity'], 'duration': 1, 'ease': 'power2.inOut',
'onUpdate': () => {
this.cameraTopviewWrapper_.position.y = tweenObject['wy'];
this.cameraTopviewWrapper_.rotation.y = tweenObject['wry'];
this.scene_.fog.density = tweenObject['fd'];
this.lights_.get('hemisphereLight').intensity = tweenObject['hli'];
},
}, 0);
timeline.to(tweenObject, {'crx': 0, 'duration': .75, 'delay': .25, 'ease': 'power2.inOut',
'onUpdate': () => {
this.cameraTopview_.rotation.x = tweenObject['crx'];
}
}, 0);
}
}
/**
* Triggers the change from the visitor view to the top view and vice versa.
* For the period of the change the interaction possibility of the user is deactivated.
* @private
*/
changeExhibitView_()
{
this.moveController_.stopInteraction();
this.worldProvider_.switchExhibitView();
}
/**
* Initializes the switch from visitor view to exhibit orbit view and vice versa.
* @private
*/
handleSwitchExhibitView_()
{
let open = this.worldProvider_.isExhibitView === true;
clearTimeout(this.viewSwitchTimeout_);
classlist.remove(open ? this.exhibitViewOpener_ : this.exhibitViewCloser_, 'is-visible');
let exhibit = this.exhibits_[this.worldProvider_.activeExhibit.id];
let elementId = this.worldProvider_.activeExhibit.elementId;
let orbitView = exhibit.getOrbitViewOfElement(elementId);
let hasHighresObject = exhibit.model.getElement(elementId).fileHighres3D.trim() != '';
// switches from visitor view to exhibit orbit view if an orbit model exists
if(open)
{
if(orbitView)
{
let artwork3D = exhibit.getArtwork3D(elementId);
this.layerProvider_.hide('exhibit');
let isLoadingHighres = hasHighresObject;
if(isLoadingHighres)
exhibit.loadHighresObject(elementId).then(() => {
if(exhibit.getHightResolutionState(elementId))
{
exhibit.switchObjectResolution(elementId, true);
this.hintBox_.hide(HintBoxContentType.ARTWORK_LOADING_HIGHRES);
}
isLoadingHighres = false;
});
this.initOrbitView_(artwork3D, orbitView).then(() => {
classlist.add(this.exhibitViewCloser_, 'is-visible');
if(isLoadingHighres)
this.hintBox_.show(HintBoxContentType.ARTWORK_LOADING_HIGHRES, 0).catch((error) => {console.warn(error);})
});
}
else
{
this.viewSwitchTimeout_ = setTimeout(() => {
classlist.add(this.exhibitViewCloser_, 'is-visible');
}, 400);
}
}
// switches from exhibit orbit view to visitor view if orbit controls were initialized
else
{
if(this.orbitControls_)
{
this.layerProvider_.hide('exhibit');
this.orbitControls_.enabled = false;
if(hasHighresObject)
{
exhibit.switchObjectResolution(elementId, false);
this.hintBox_.hide(HintBoxContentType.ARTWORK_LOADING_HIGHRES);
}
this.leaveOrbitView_().then(() => {
classlist.add(this.exhibitViewOpener_, 'is-visible');
if(!this.hintBox_.contentWasShown(HintBoxContentType.ARTWORK_INFO))
this.hintBox_.show(HintBoxContentType.ARTWORK_INFO).catch((error) => {console.warn(error);});
});
}
else
{
this.viewSwitchTimeout_ = setTimeout(() => {
classlist.add(this.exhibitViewOpener_, 'is-visible');
}, 400);
}
}
}
/**
* Starts the animated change from the visitor view to the exhibit orbit view
* @param {THREE.Object3D} artwork3D Artwork which should be the center of the orbit view.
* @param {ExhibitionElementOrbitViewModel} orbitView
* @return {Promise}
* @private
*/
initOrbitView_(artwork3D, orbitView)
{
this.autoResetProvider_.cancel();
this.visitor_.controlsAreEnabled = false;
let orbitCenter = artwork3D.getWorldPosition(new THREE.Vector3());
orbitCenter.add(orbitView.center);
let vX = this.visitor_.object3D.position.x - orbitCenter.x;
let vZ = this.visitor_.object3D.position.z - orbitCenter.z;
let magV = Math.sqrt(vX*vX + vZ*vZ);
let closestOrbitPoint = new THREE.Vector3(
orbitCenter.x + vX / magV * orbitView.distance,
0,
orbitCenter.z + vZ / magV * orbitView.distance
);
if(!this.cameraOribitview_)
{
this.cameraOribitview_ = /** @type {THREE.Camera} */ (this.camera_.clone());
this.cameraOribitview_.name = 'orbitCamera';
this.scene_.add(this.cameraOribitview_);
}
this.cameraOribitview_.position.x = closestOrbitPoint.x;
this.cameraOribitview_.position.y = orbitCenter.y;
this.cameraOribitview_.position.z = closestOrbitPoint.z;
this.cameraOribitview_.rotation.y = 0;
this.cameraOribitview_.lookAt(orbitCenter);
return this.animateToClosestOrbitPoint_(closestOrbitPoint, orbitCenter.y, 0, 0).then(() => {
this.changeCamera_(this.cameraOribitview_);
this.orbitControls_ = new THREE.OrbitControls(this.camera_, this.renderer_.domElement);
this.orbitControls_.target.set(orbitCenter.x, orbitCenter.y, orbitCenter.z);
this.orbitControls_.minDistance = orbitView.limitDistance ? orbitView.limitDistance.start : .5;
this.orbitControls_.maxDistance = orbitView.limitDistance ? orbitView.limitDistance.end : 8;
this.orbitControls_.minAzimuthAngle = orbitView.limitAzimuth ? orbitView.limitAzimuth.start * Math.PI / 180 : Infinity;
this.orbitControls_.minAzimuthAngle = orbitView.limitAzimuth ? orbitView.limitAzimuth.end * Math.PI / 180 : Infinity;
this.orbitControls_.minPolarAngle = (orbitView.limitPolar ? orbitView.limitPolar.start : 30) * Math.PI / 180;
this.orbitControls_.maxPolarAngle = (orbitView.limitPolar ? orbitView.limitPolar.end : 95) * Math.PI / 180;
this.orbitControls_.enablePan = true;
this.orbitControls_.enableDamping = true;
this.orbitControls_.update();
this.savedProperties_.set('orbitZoomLevel', this.orbitControls_.getZoomLevel());
this.savedProperties_.set('orbitCenterX', orbitCenter.x);
this.savedProperties_.set('orbitCenterY', orbitCenter.y);
this.savedProperties_.set('orbitCenterZ', orbitCenter.z);
this.orbit_.azimuthalAngle = this.orbitControls_.getAzimuthalAngle();
this.orbit_.polarAngle = this.orbitControls_.getPolarAngle();
this.orbit_.zoom = this.orbitControls_.getZoomLevel();
this.autoResetProvider_.start();
});
}
/**
* Starts the animated change from the visitor view to the exhibit orbit view
* @param {THREE.Vector3} closestOrbitPoint Closest vector form the visitor position to the default obit view radius (zoom).
* @param {number} targetCameraY
* @param {number} targetCameraZ
* @param {number} targetCameraRotationX
* @return {Promise}
* @private
*/
animateToClosestOrbitPoint_(closestOrbitPoint, targetCameraY, targetCameraZ, targetCameraRotationX)
{
let completer = new Completer();
let startQuaternion = this.visitor_.object3D.quaternion;
let targetQuaternion = this.cameraOribitview_.quaternion;
let tweenObject = {
't': 0,
'x': this.visitor_.physicBody.position.x,
'z': this.visitor_.physicBody.position.z,
'cy': this.visitor_.camera.position.y,
'cz': this.visitor_.camera.position.z,
'crx': this.visitor_.camera.rotation.x,
};
let distancePoint = Math.sqrt(Math.pow(tweenObject.x - closestOrbitPoint.x,2) + Math.pow(tweenObject.z - closestOrbitPoint.z,2));
let distanceCamera = Math.max(Math.abs(this.visitor_.camera.position.y - targetCameraY), Math.abs(this.visitor_.camera.position.z - targetCameraZ));
gsap.to(tweenObject,{
't': 1,
'x': closestOrbitPoint.x,
'z': closestOrbitPoint.z,
'cy': targetCameraY,
'cz': targetCameraZ,
'crx': targetCameraRotationX,
'duration': Math.max(distancePoint, distanceCamera),
'ease': 'power2.inOut',
'onUpdate': () => {
//position
this.visitor_.physicBody.position.x = tweenObject['x'];
this.visitor_.physicBody.position.z = tweenObject['z'];
//camera
this.visitor_.camera.position.y = tweenObject['cy'];
this.visitor_.camera.position.z = tweenObject['cz'];
this.visitor_.camera.rotation.x = tweenObject['crx'];
//rotation
let qm = startQuaternion.clone();
qm.slerp(targetQuaternion, tweenObject['t']);
this.visitor_.object3D.rotation.setFromQuaternion(qm);
},
'onComplete': () => {
completer.resolve();
}
});
return completer.getPromise();
}
/**
* Starts the animated change from the exhibit orbit view back to the visitor view
* @return {Promise}
* @private
*/
leaveOrbitView_()
{
let completer = new Completer();
this.orbitControls_.enabled = false;
this.autoResetProvider_.cancel();
let tweenObject1 = {
'azimuthAngle': this.orbitControls_.getAzimuthalAngle(),
'polarAngle': this.orbitControls_.getPolarAngle(),
'zoom': this.orbitControls_.getZoomLevel(),
};
let tweenObject2 = {
'y': this.visitor_.camera.position.y
};
let distancePolarAngle = Math.abs((90 * Math.PI / 180) - tweenObject1['polarAngle']);
let timeline = gsap.timeline({'ease': 'power2.inOut', 'onComplete': () => {completer.resolve();}});
timeline.to(tweenObject1,{
'polarAngle': 90 * Math.PI / 180,
'zoom': this.savedProperties_.get('orbitZoomLevel'),
'duration': distancePolarAngle,
'ease': 'none',
'onUpdate': () => {
this.orbitControls_.updateToPoint(tweenObject1['azimuthAngle'], tweenObject1['polarAngle'], tweenObject1['zoom'])
},
'onComplete': () => {
this.visitor_.physicBody.position.x = this.cameraOribitview_.position.x;
this.visitor_.physicBody.position.z = this.cameraOribitview_.position.z;
this.visitor_.object3D.rotation.y = this.orbitControls_.getAzimuthalAngle();
this.changeCamera_(this.visitor_.camera);
this.orbitControls_.dispose();
this.orbitControls_ = null;
}
});
if(this.visitor_.camera.position.y != this.visitor_.cameraNormalY)
{
let distanceCameraY = Math.abs(this.visitor_.camera.position.y - this.visitor_.cameraNormalY);
timeline.to(tweenObject2, {
'y': this.visitor_.cameraNormalY,
'duration': distanceCameraY * .5,
'ease': 'none',
'onUpdate': () => {
this.visitor_.camera.position.y = tweenObject2['y'];
}
});
}
return completer.getPromise().then(() => {
if(!this.isFirstActivation_) this.visitor_.controlsAreEnabled = true;
this.autoResetProvider_.start();
})
}
/**
* Initializes the display of the layer for the external installation of an exhibit.
* Rendering is paused during display to save performance.
* @private
*/
showExhibitFrame_()
{
this.autoResetProvider_.cancel();
this.visitor_.controlsAreEnabled = false;
this.worldProvider_.pauseRendering();
this.videoProvider_.pauseAllPlayers();
let elementModel = this.exhibitionProvider_.items[this.worldProvider_.activeExhibit.id].getElement(this.worldProvider_.activeExhibit.elementId);
this.layerProvider_.show('installation', {'url': elementModel.externUrl});
}
/**
* Disables the background sound when a video is started.
* @private
*/
handleVideoPlay_()
{
this.soundProvider_.enableBackgroundSound = false;
}
/**
* Enables the background sound when a video is paused/stopped.
* @private
*/
handleVideoPause_()
{
if(window['SOUND_MODE'] != 'generic-platform')
this.soundProvider_.enableBackgroundSound = true;
}
/**
* Monitors whether a layer is opened and performs different actions depending on the layer.
* @param {LayerEvent} event
* @private
*/
handleLayerShow_(event)
{
switch(event.layer.type)
{
case LayerType.VISITOR:
// hides navigation elements on the left viewport side (under the visitor layer)
if(window['SOUND_MODE'] == 'generic-platform') this.soundProvider_.enableBackgroundSound = false;
clearTimeout(this.viewSwitchTimeout_);
classlist.remove(this.exhibitViewOpener_, 'is-visible');
classlist.remove(this.exhibitViewCloser_, 'is-visible');
classlist.remove(this.exhibitFrameOpener_, 'is-visible');
classlist.remove(this.exhibitInteractiveHint_, 'is-visible');
break;
case LayerType.EXHIBIT:
// hides navigation elements on the right viewport side (under the exhibit layer)
if(window['SOUND_MODE'] == 'generic-platform') this.soundProvider_.enableBackgroundSound = false;
classlist.remove(this.exhibitInfoOpener_, 'is-visible');
break;
case LayerType.MEDIA:
// hides the visitor layer which would be overlapping the current media layer
if(window['SOUND_MODE'] == 'generic-platform') this.soundProvider_.enableBackgroundSound = false;
this.layerProvider_.hide('visitor');
this.visitor_.controlsAreEnabled = false;
break;
case LayerType.INSTALLATION:
// disables the background sound when a the installation frame is opened
this.soundProvider_.enableBackgroundSound = false;
break;
}
}
/**
* Monitors whether a layer is closed and performs different actions depending on the layer.
* @param {LayerEvent} event
* @private
*/
handleLayerHide_(event)
{
switch(event.layer.type)
{
case LayerType.VISITOR:
// depending of the current view make the orbit view close, the orbit view opener or the external installation button visible
if(this.worldProvider_.activeExhibit != null)
{
let open = this.worldProvider_.isExhibitView === true;
let elementModel = this.exhibitionProvider_.items[this.worldProvider_.activeExhibit.id].getElement(this.worldProvider_.activeExhibit.elementId);
if(elementModel)
{
if(elementModel.orbitView)
classlist.add(open ? this.exhibitViewCloser_ : this.exhibitViewOpener_, 'is-visible');
else if(elementModel.externUrl)
classlist.add(this.exhibitFrameOpener_, 'is-visible');
}
}
break;
case LayerType.EXHIBIT:
// when no installation frame is open and the hit box for the artwork info wasn't shown yet display it
if(!this.worldProvider_.isExhibitView)
{
if(!this.hintBox_.contentWasShown(HintBoxContentType.ARTWORK_INFO))
this.hintBox_.show(HintBoxContentType.ARTWORK_INFO).catch((error) => {console.warn(error);});
}
// show the exhibit info button
if(this.worldProvider_.activeExhibit)
classlist.add(this.exhibitInfoOpener_, 'is-visible');
break;
case LayerType.MEDIA:
// activate the visitor move interaction if the view is not the top view or the exhibit orbit view and the reshuffeling is over
if(!this.worldProvider_.isTopView && !this.worldProvider_.isExhibitView && !this.isFirstActivation_)
this.visitor_.controlsAreEnabled = true;
break;
case LayerType.INSTALLATION:
// activate the visitor move interaction if the view is not the top view or the exhibit orbit view and the reshuffeling is over
if(!this.worldProvider_.isTopView && !this.worldProvider_.isExhibitView && !this.isFirstActivation_)
this.visitor_.controlsAreEnabled = true;
this.autoResetProvider_.start();
// restarts the rendering of the scene
this.startRenderer_();
break;
}
// enables the background sound when the visitor, media, exhibit and installation layer are closed
if (!this.layerProvider_.isOpen('visitor') && !this.layerProvider_.isOpen('exhibit') &&
!this.layerProvider_.isOpen('media') && !this.layerProvider_.isOpen('installation')) {
this.soundProvider_.enableBackgroundSound = true;
}
}
/**
* Changes the camera to the passed one and updates the matrix to the viewport/render size.
* @param {THREE.Camera} camera
* @private
*/
changeCamera_(camera)
{
this.camera_ = camera;
this.updateCameraMatrix_();
}
/**
* Activates the attraction of the exhibits, so that the physical calculation of Cannon affects their mass and thus the position.
* @param {boolean} enable
* @private
*/
enableGravity_(enable)
{
if(!enable)
{
this.exhibits_.forEach((exhibit) => {
exhibit.attraction = 0;
});
}
}
/**
* Monitors the Cannon sensors and their interaction start. Checks if the visitor interacts with exhibit elements.
* If it is an activity zone, it is activated and triggers further events (rearrangement of other artworks,
* user profile update, display of exhibit view and info buttons).
* @param {Object} event
* @private
*/
handleCollisionStart_(event)
{
if((!this.visitor_.controlsAreEnabled && this.allMassBodiesAreScalledUp_) || this.worldProvider_.isExhibitView)
return;
let name = this.exhibitionProvider_.physicBodyReferences.get(event['body']['id']);
let activeExhibit = null;
if(name)
{
let nameSplitObj = name.split('::');
if(nameSplitObj[0] == 'mass')
{
let exhibitId = parseInt(nameSplitObj[1], 10);
this.exhibits_[exhibitId].enteredByVisitor = true;
}
else if(nameSplitObj[0] == 'sensor')
{
let exhibitId = parseInt(nameSplitObj[1], 10);
let elementId = parseInt(nameSplitObj[2], 10);
if(this.collidedSensors_.indexOf(exhibitId+"::"+elementId) == -1)
{
this.collidedSensors_.push(exhibitId + "::" + elementId);
//console.log("ACTIVATE exhibit:" + exhibitId + " element:" + elementId);
if (activeExhibit)
{
let distanceActiveExhibit = this.exhibits_[activeExhibit.id].getInteractionDistance(activeExhibit.elementId, this.visitor_.object3D.position);
let distanceCurrentExhibit = this.exhibits_[exhibitId].getInteractionDistance(elementId, this.visitor_.object3D.position);
if (distanceCurrentExhibit < distanceActiveExhibit)
{
activeExhibit = {id: exhibitId, elementId: elementId};
}
}
else
{
activeExhibit = {id: exhibitId, elementId: elementId};
}
}
}
}
if(activeExhibit)
{
if(this.activationCountdown_ > 0 || this.allMassBodiesAreScalledUp_ == false)
this.virtualActiveSensor_ = {exhibitId: activeExhibit.id, elementId: activeExhibit.elementId};
else
this.activateSensor_(activeExhibit.id, activeExhibit.elementId);
}
}
/**
* Monitors the Cannon sensors and their interaction end. When the user leaves an activity zone,
* further actions are triggered (end of movement of other artwork, exhibit view and info buttons are hidden).
* @param {Object} event
* @private
*/
handleCollisionEnd_(event)
{
if((!this.visitor_.controlsAreEnabled && this.allMassBodiesAreScalledUp_) || this.worldProvider_.isExhibitView || !event['bodyA'] || !event['bodyB'])
return;
let nameA = this.exhibitionProvider_.physicBodyReferences.get(event['bodyA']['id']);
let nameB = this.exhibitionProvider_.physicBodyReferences.get(event['bodyB']['id']);
if(nameA && nameB && (nameA == 'visitor' || nameB == 'visitor'))
{
let name = nameA == 'visitor' ? nameB : nameA;
let nameSplitObj = name.split('::');
if(nameSplitObj[0] == 'mass')
{
let exhibitId = parseInt(nameSplitObj[1], 10);
this.exhibits_[exhibitId].enteredByVisitor = false;
}
else if(nameSplitObj[0] == 'sensor')
{
let exhibitId = parseInt(nameSplitObj[1], 10);
let elementId = parseInt(nameSplitObj[2], 10);
this.collidedSensors_.splice(this.collidedSensors_.indexOf(exhibitId+"::"+elementId), 1);
//console.log("DEACTIVATE exhibit:"+exhibitId+" element:"+elementId, this.collidedSensors_);
}
}
if(this.collidedSensors_.length > 0)
{
let activeExhibit = null;
for(let i=0; i<this.collidedSensors_.length; i++)
{
let collidedSensorObj = this.collidedSensors_[i].split('::');
let exhibitId = parseInt(collidedSensorObj[0], 10);
let elementId = parseInt(collidedSensorObj[1], 10);
if (activeExhibit)
{
let distanceActiveExhibit = this.exhibits_[activeExhibit.id].getInteractionDistance(activeExhibit.elementId, this.visitor_.object3D.position);
let distanceCurrentExhibit = this.exhibits_[exhibitId].getInteractionDistance(elementId, this.visitor_.object3D.position);
if (distanceCurrentExhibit < distanceActiveExhibit)
activeExhibit = {id: exhibitId, elementId: elementId};
}
else
activeExhibit = {id: exhibitId, elementId: elementId};
}
if(this.activationCountdown_ > 0 || this.allMassBodiesAreScalledUp_ == false)
this.virtualActiveSensor_ = {exhibitId: activeExhibit.id, elementId: activeExhibit.elementId};
else
this.activateSensor_(activeExhibit.id, activeExhibit.elementId);
}
else
{
this.virtualActiveSensor_ = null;
this.deactivateSensor_();
}
}
/**
* Entering an activity zone is registered.
* @param {number} exhibitId
* @param {number} elementId
* @private
*/
activateSensor_(exhibitId, elementId)
{
if(!this.worldProvider_.activeExhibit || (this.worldProvider_.activeExhibit.id != exhibitId && this.worldProvider_.activeExhibit.elementId != elementId))
{
let elementModel = this.exhibitionProvider_.items[exhibitId].getElement(elementId);
if(elementModel.orbitView)
classlist.add(this.exhibitViewOpener_, 'is-visible');
else if(elementModel.externUrl)
classlist.add(this.exhibitFrameOpener_, 'is-visible');
classlist.enable(this.exhibitInteractiveHint_, 'is-second-info', elementModel.orbitView != null || elementModel.externUrl != '');
this.exhibits_[exhibitId].activatedByVisitor = true;
// updates the visitor preferences
this.exhibitionProvider_.addPreference(exhibitId, elementId);
this.worldProvider_.activateExhibit(exhibitId, elementId);
classlist.add(document.documentElement, 'activated-exhibit');
classlist.add(this.exhibitInfoOpener_, 'is-visible');
this.layerProvider_.hide('visitor');
setTimeout(() => {
this.showNavigationHints_().then(() => {
if(!this.hintBox_.contentWasShown(HintBoxContentType.ARTWORK_INFO))
this.hintBox_.show(HintBoxContentType.ARTWORK_INFO).catch((error) => {console.warn(error);});
else if(!this.hintBox_.contentWasShown(HintBoxContentType.ARTWORK_SINGLE_VIEW) && elementModel.orbitView)
this.hintBox_.show(HintBoxContentType.ARTWORK_SINGLE_VIEW).catch((error) => {console.warn(error);});
}, (error) => {console.warn(error);});
}, 500);
if(this.activationCountdown_ == 0 && this.allMassBodiesAreScalledUp_ == true)
this.activateGravity_();
}
}
/**
* Activates the attraction force when the user is in an exhibit activation zone. The other exhibits in the vicinity
* of the activated exhibit are rearranged according to their attraction force based on the tags.
* @private
*/
activateGravity_()
{
if(this.isFirstActivation_) {
this.isFirstActivation_ = false;
this.checkExhibits_ = true;
}
if(this.worldProvider_.activeExhibit) {
this.exhibits_[this.worldProvider_.activeExhibit.id].setImmovable(true);
}
this.enableGravity_(true);
dataset.set(this.activationHint_, 'state', 'profile');
classlist.enable(this.activationHint_, 'is-visible', true);
}
/**
* Leaving an activity zone is registered.
* @private
*/
deactivateSensor_()
{
if(this.worldProvider_.activeExhibit != null)
{
classlist.remove(this.exhibitViewOpener_, 'is-visible');
classlist.remove(this.exhibitFrameOpener_, 'is-visible');
classlist.remove(this.exhibitInteractiveHint_, 'is-visible');
this.exhibitionProvider_.visitor.completeCurrentPreference();
this.exhibits_[this.worldProvider_.activeExhibit.id].activatedByVisitor = false;
if(this.activationCountdown_ == 0 && this.allMassBodiesAreScalledUp_ == true)
{
this.enableGravity_(false);
this.exhibits_[this.worldProvider_.activeExhibit.id].setImmovable(false);
classlist.remove(this.activationHint_, 'is-visible');
}
this.worldProvider_.deactivateExhibit();
classlist.remove(document.documentElement, 'activated-exhibit');
classlist.remove(this.exhibitInfoOpener_, 'is-visible');
this.layerProvider_.hide('exhibit');
}
}
/**
* Monitor the step before the new powers are applied to physical objects. Influences the forces that occur.
* @private
*/
handlePhysicPreStep_()
{
let allMassBodiesAreScalledUp = this.activationCountdown_ == 0;
this.exhibits_.forEach((exhibit) => {
if (exhibit.attraction != 0 && exhibit.physicMassBodyScale == 1)
{
if(!exhibit.activatedByVisitor)
{
let forceDirection = new THREE.Vector3();
forceDirection.subVectors(this.visitor_.object3D.position, exhibit.object3D.position).normalize();
let force = new CANNON.Vec3(
forceDirection.x * this.worldProvider_.settings.physic['attractionSpeed'],
0,
forceDirection.z * this.worldProvider_.settings.physic['attractionSpeed']
);
if(exhibit.physicMassBody)
exhibit.physicMassBody.force.set(force.x, force.y, force.z);
}
}
const condition = this.activationCountdown_ == 0;
if(condition && exhibit.physicMassBodyScale < 1)
{
exhibit.updateMassBodyScale(Math.min(exhibit.physicMassBodyScale + this.worldProvider_.settings.physic['scaleSpeed'], 1));
allMassBodiesAreScalledUp = false;
}
});
const activationTime = 5;
// Runs once when all exhibits have been fully scaled up. Checks if a saved virtual sensor should be activated now or it starts displaying the navigation hints.
if(this.allMassBodiesAreScalledUp_ == false && allMassBodiesAreScalledUp == true)
{
setTimeout(() => {
let timeline = gsap.timeline({'onComplete': () => {
this.enableDebugRendering_ = window['PHYSIC_MODE'] === 'on';
if(this.enableDebugRendering_ === false)
{
/** @type {Array<THREE.Mesh>} */ (this.physicDebugger_['getMeshes']()).forEach((mesh) => {
mesh.parent.remove(mesh);
});
}
}});
/** @type {Array<THREE.Mesh>} */ (this.physicDebugger_['getMeshes']()).forEach((mesh) => {
timeline.add(gsap.to(mesh.material, {'opacity': window['PHYSIC_MODE'] === 'on' ? 1 : 0, 'duration': 1, 'ease': 'power2.out'}), 0);
});
}, activationTime * 1000);
this.visitor_.controlsAreEnabled = true;
classlist.remove(this.activationHint_, 'is-visible');
this.hintBox_.hide();
if(this.virtualActiveSensor_)
{
this.activateSensor_(this.virtualActiveSensor_.exhibitId, this.virtualActiveSensor_.elementId);
this.activateGravity_();
this.virtualActiveSensor_ = null;
} else {
setTimeout(() => { this.showNavigationHints_().catch((error) => {console.warn(error);}); }, 500);
}
// Activates the frame check (only for mobile devices) for the performance monitoring.
if(this.worldProvider_.isMobileDevice)
setTimeout(() => {this.checkFrames_ = true;}, activationTime * 1000);
}
this.allMassBodiesAreScalledUp_ = allMassBodiesAreScalledUp;
}
/**
* Main call for 3D scene updates in the render process. Updates all relevant 3D and Cannon Objects influenced
* by the visitor movement, the artwork activation and the physical attraction.
* @inheritDoc
*/
updateScene_(delta)
{
super.updateScene_(delta);
this.moveController_.checkGamepad(null);
// updating the visitor by the interaction detected in the move controller
let velocityMoveSide = this.moveController_.getAcceleration('x') * delta;
let velocityMoveStraight = this.moveController_.getAcceleration('z') * delta;
if(this.visitor_.controlsAreEnabled && this.moveController_.isMoving)
this.autoResetProvider_.start();
if (this.visitor_.controlsAreEnabled && !this.worldProvider_.isTopView && !this.worldProvider_.isExhibitView && !this.layerProvider_.isOpen('guiding'))
{
// visitor movement
const isTouchZooming = this.moveController_.isZoomingIn == MoveType.TOUCH_ZOOM_IN || this.moveController_.isZoomingOut == MoveType.TOUCH_ZOOM_OUT;
const couldMovePerTouchLeft = false;
const couldMovePerTouchRight = false;
const couldMoveLeft = this.moveController_.isMovingLeft && (this.moveController_.isMovingLeft != MoveType.TOUCH_LEFT || couldMovePerTouchLeft);
const couldMoveRight = this.moveController_.isMovingRight && (this.moveController_.isMovingRight != MoveType.TOUCH_RIGHT || couldMovePerTouchRight);
if (couldMoveLeft && !isTouchZooming && this.moveController_.isMovingLeft != MoveType.MOUSE_LEFT && this.moveController_.isMovingLeft != MoveType.PAD_LEFT)
this.moveController_.velocity.x += velocityMoveSide;
if (couldMoveRight && !isTouchZooming && this.moveController_.isMovingRight != MoveType.MOUSE_RIGHT && this.moveController_.isMovingRight != MoveType.PAD_RIGHT)
this.moveController_.velocity.x -= velocityMoveSide;
if (this.moveController_.isMovingUp && !isTouchZooming && this.moveController_.isMovingUp != MoveType.MOUSE_UP && this.moveController_.isMovingUp != MoveType.PAD_UP)
this.moveController_.velocity.z += velocityMoveStraight;
if (this.moveController_.isMovingDown && !isTouchZooming && this.moveController_.isMovingDown != MoveType.MOUSE_DOWN && this.moveController_.isMovingDown != MoveType.PAD_DOWN)
this.moveController_.velocity.z -= velocityMoveStraight;
// visitor rotation
let rh = 0;
if(window['GAMEPAD_MODE'] === 'on' && this.moveController_.gamepadIsEnabled)
{
let horizontalAngle = this.moveController_.gamepadAxes.get(MoveType.PAD_RIGHT_HORIZONTAL);
horizontalAngle = Math.abs(Math.round(horizontalAngle * 10)) > 1 ? horizontalAngle : 0;
let verticalAngle = this.moveController_.gamepadAxes.get(MoveType.PAD_RIGHT_VERTICAL);
verticalAngle = Math.abs(Math.round(verticalAngle * 10)) > 1 ? verticalAngle : 0;
this.moveController_.velocity['rh'] += -horizontalAngle * this.moveController_.getAcceleration('rh') / 1000 * delta;
this.moveController_.velocity['rv'] += -verticalAngle * this.moveController_.getAcceleration('rv') / 1000 * delta;
rh = this.visitor_.object3D.rotation.y + this.moveController_.velocity['rh'];
}
else
{
if(this.moveController_.interactionMovePoints)
{
if(this.moveController_.isMouseMoved)
{
this.moveController_.velocity['rh'] = -this.moveController_.interactionMoveDistances[0].x * this.moveController_.getAcceleration('rh') / 1000 * delta;
this.moveController_.velocity['rv'] = -this.moveController_.interactionMoveDistances[0].y * this.moveController_.getAcceleration('rv') / 1000 * delta;
}
else
{
if(!isTouchZooming){
if(this.moveController_.isMovingLeft == MoveType.TOUCH_LEFT)
this.moveController_.velocity['rh'] = Math.min(7, this.moveController_.touchStartMoveDistance.x / 10) * this.moveController_.getAcceleration('rh') / 1000 * delta;
else if(this.moveController_.isMovingRight == MoveType.TOUCH_RIGHT)
this.moveController_.velocity['rh'] = -Math.min(7, this.moveController_.touchStartMoveDistance.x / 10) * this.moveController_.getAcceleration('rh') / 1000 * delta;
else
this.moveController_.velocity['rh'] = -this.moveController_.interactionMoveDistances[0].x * this.moveController_.getAcceleration('rh') / 1000 * delta;
} else {
this.moveController_.velocity['rv'] = this.moveController_.interactionZoomDistance * this.moveController_.getAcceleration('rv') / 1000 * delta;
}
}
this.moveController_.interactionMoveDistances[0] = new Coordinate(0, 0);
}
rh = this.visitor_.object3D.rotation.y + this.moveController_.velocity['rh'];
}
const max = this.visitor_.cameraPolarAngleRange.start > this.visitor_.cameraPolarAngleRange.end ? this.visitor_.cameraPolarAngleRange.end : this.visitor_.cameraPolarAngleRange.start;
const min = this.visitor_.cameraPolarAngleRange.start > this.visitor_.cameraPolarAngleRange.end ? this.visitor_.cameraPolarAngleRange.start : this.visitor_.cameraPolarAngleRange.end;
this.visitor_.camera.rotation.x = Math.max(max, Math.min(min, this.visitor_.camera.rotation.x + this.moveController_.velocity['rv']));
this.visitor_.object3D.rotation.y = rh;
}
// depending camera x and z position on current camera x rotation
if(this.visitor_.controlsAreEnabled && !this.worldProvider_.isExhibitView && !this.layerProvider_.isOpen('guiding'))
{
this.visitor_.camera.position.y = ((this.visitor_.camera.rotation.x - this.visitor_.cameraPolarAngleRange.start) / (this.visitor_.cameraPolarAngleRange.end - this.visitor_.cameraPolarAngleRange.start)) * (this.visitor_.cameraRangeY.end - this.visitor_.cameraRangeY.start) + this.visitor_.cameraRangeY.start;
this.visitor_.camera.position.z = parseFloat(Math.min(0, this.visitor_.camera.rotation.x) * -10);
}
let moveDirectionFrontal = new THREE.Vector3();
this.visitor_.camera.getWorldDirection(moveDirectionFrontal);
let moveDirectionSide = moveDirectionFrontal.clone().applyAxisAngle(new THREE.Vector3(0,1,0), 90 * Math.PI / 180);
moveDirectionFrontal.multiplyScalar(this.moveController_.velocity.z * delta);
moveDirectionSide.multiplyScalar(this.moveController_.velocity.x * delta);
let moveDirection = moveDirectionFrontal.clone().add(moveDirectionSide);
// checks collision via raycaster
let collidedInNextStep = false;
let height = 1;
let visitorPosition = new THREE.Vector3(
this.visitor_.physicBody.position.x + Math.round(moveDirection.x * 100) / 100,
height,
this.visitor_.physicBody.position.z + Math.round(moveDirection.z * 100) / 100
);
let nextVisitorPosition = new THREE.Vector3(
visitorPosition.x + Math.round(moveDirection.x * 100) / 100,
height,
visitorPosition.z + Math.round(moveDirection.z * 100) / 100
);
let exhibitColliders = [];
let minDistanceVisitorToExhibit = this.visitor_.maxDistanceToExhibit;
this.exhibits_.forEach((exhibit) => {
minDistanceVisitorToExhibit = Math.min(minDistanceVisitorToExhibit, nextVisitorPosition.distanceTo(exhibit.object3D.position));
if(this.allMassBodiesAreScalledUp_)
{
if(exhibit.isActive) {
if(exhibit.shouldTestColliding([visitorPosition, nextVisitorPosition]))
exhibitColliders = exhibitColliders.concat(exhibit.collisionObjects);
} else {
exhibit.enableInteraction();
}
}
});
// checks if new pos is outside the visitor max distance to any exhibit
if(minDistanceVisitorToExhibit >= this.visitor_.maxDistanceToExhibit)
{
collidedInNextStep = true;
nextVisitorPosition.x = this.visitor_.physicBody.position.x;
nextVisitorPosition.z = this.visitor_.physicBody.position.z;
}
if(!(nextVisitorPosition.x == this.visitor_.physicBody.position.x && nextVisitorPosition.z == this.visitor_.physicBody.position.z))
{
let direction = new THREE.Vector3();
let distance = visitorPosition.distanceTo(nextVisitorPosition);
direction.subVectors(nextVisitorPosition, visitorPosition).normalize();
this.raycaster_.set(visitorPosition.clone(), direction.clone());
this.raycaster_.far = distance + this.visitor_.radius;
let objects = this.allMassBodiesAreScalledUp_ ? exhibitColliders : this.allExhibitColliders_;
if(objects.length > 0)
{
let intersects = this.raycaster_.intersectObjects(objects);
collidedInNextStep = intersects.length > 0;
}
}
if(!collidedInNextStep)
{
this.visitor_.physicBody.position.x += moveDirection.x;
this.visitor_.physicBody.position.z += moveDirection.z;
}
else
{
this.moveController_.velocity.x = 0;
this.moveController_.velocity.z = 0;
}
// apply visitor friction (smooth movement)
let frictionalImpulse = -this.moveController_.velocity.x * this.moveController_.getFriction('x');
this.moveController_.velocity.x += frictionalImpulse * delta;
frictionalImpulse = -this.moveController_.velocity.z * this.moveController_.getFriction('z');
this.moveController_.velocity.z += frictionalImpulse * delta;
if(!this.moveController_.interactionMovePoints)
{
frictionalImpulse = -this.moveController_.velocity['rh'] * this.moveController_.getFriction('rh');
this.moveController_.velocity['rh'] += frictionalImpulse * delta;
frictionalImpulse = -this.moveController_.velocity['rv'] * this.moveController_.getFriction('rv');
this.moveController_.velocity['rv'] += frictionalImpulse * delta;
}
// update the physical world
let physicWorldDelta = Math.min(delta, 0.1);
this.visitor_.physicBody.position.y = 0;
if(this.worldProvider_.physicIsEnabled)
this.physicWorld_.step(this.worldProvider_.settings.physic['fixedTimeStep'], physicWorldDelta, this.worldProvider_.settings.physic['maxSubSteps']);
// checks once if in the exhibits certain 3D elements can be removed
if(this.checkExhibits_)
{
this.checkExhibits_ = false;
this.exhibits_.forEach((exhibit) => {
exhibit.checkObjectsAfterFirstInteraction();
});
}
// update physical instance of the visitor
this.visitor_.physicBody.position.y = 0;
// update user profil
this.exhibitionProvider_.visitor.updateProfile(delta, /** @type {number} */ (this.worldProvider_.settings.profile['changeMultiplier']));
let equalityList = [];
// optical match of the optical THREE elements to the Cannon physical objects
this.visitor_.object3D.position.x = this.visitor_.physicBody.position.x;
this.visitor_.object3D.position.z = this.visitor_.physicBody.position.z;
this.visitor_.update();
this.exhibits_.forEach((exhibit) => {
exhibit.update(this.clock_);
// update attraction
if(this.worldProvider_.activeExhibit != null)
{
equalityList.push({'id': exhibit.id,'value': this.exhibitionProvider_.visitor.caluculateEqualityFactor(exhibit.model.tags)});
}
});
// apply attraction
if(equalityList.length > 0)
{
equalityList.sort((a, b) => {return b['value'] - a['value']});
for (let i = 0; i < equalityList.length; i++){
let attraction = Math.pow(0.5, (1 / this.worldProvider_.settings.physic['attractionSpeed']) * i);
this.exhibits_[equalityList[i]['id']].attraction = attraction;
}
}
// update orbit controls if the exhibit orbit view is active
if(this.orbitControls_)
{
this.orbitControls_.update();
let newAzimuthalAngle = this.orbitControls_.getAzimuthalAngle();
let newPolarAngle = this.orbitControls_.getPolarAngle();
let newZoom = this.orbitControls_.getZoomLevel();
let userIsInactive = this.orbit_.azimuthalAngle == newAzimuthalAngle && this.orbit_.polarAngle == newPolarAngle && this.orbit_.zoom == newZoom;
if(!userIsInactive)
this.autoResetProvider_.start();
this.orbit_.azimuthalAngle = newAzimuthalAngle;
this.orbit_.polarAngle = newPolarAngle;
this.orbit_.zoom = newZoom;
}
// update ground
this.ground_.position.x = this.visitor_.object3D.position.x;
this.ground_.position.z = this.visitor_.object3D.position.z;
// update activation counter
if(this.countdownIsRunning_)
{
this.activationCountdown_ = Math.max(this.activationCountdown_ - delta, 0);
/** @type {!Element} */ (this.activationHint_.querySelector('.activation-hint-counter')).innerHTML = Math.round(this.activationCountdown_).toString();
if(this.activationCountdown_ == 0)
{
setTimeout(() => {
/** @type {!Element} */ (this.activationHint_.querySelector('.activation-hint-counter')).innerHTML = '';
}, 1000);
let timeline = gsap.timeline();
/** @type {Array<THREE.Mesh>} */ (this.physicDebugger_['getMeshes']()).forEach((mesh) => {
timeline.add(gsap.to(mesh.material, {'opacity': 1, 'duration': 1, 'ease': 'power2.out'}), 0);
});
this.exhibits_.forEach((exhibit) => {
exhibit.updateVisibilities();
});
this.enableDebugRendering_ = true;
this.countdownIsRunning_ = false;
}
}
// update Cannon physic debugger
if(this.enableDebugRendering_)
this.physicDebugger_.update();
this.renderer_.render(this.scene_, this.camera_);
}
}
exports = Exhibition;