goog.module('gep.components.Abstract3DWorld');
const {Component} = goog.require('clulib.cm');
const Completer = goog.require('clulib.async.Completer');
const ResizeProvider = goog.require('nbsrc.provider.ResizeProvider');
const {WorldProvider,WorldEventType} = goog.require('gep.provider.WorldProvider');
const {listen,removeAll,EventType} = goog.require('goog.events');
const {setStyle} = goog.require('goog.style');
const classlist = goog.require('goog.dom.classlist');
/**
* Abstracte class to create and monitor the THREE renderer.
* Component is used from {@Link https://www.npmjs.com/package/clulib}
* @extends {Component}
*/
class Abstract3DWorld extends Component
{
constructor()
{
super();
/**
* Reference to ResizeProvider for monitoring device size changes.
* @type {ResizeProvider}
* @protected
*/
this.resizeProvider_ = ResizeProvider.getInstance();
/**
* Reference to the WorldProvider for monitoring actions, loading processes and status changes of the created THREE.Scene.
* @type {WorldProvider}
* @protected
*/
this.worldProvider_ = WorldProvider.getInstance();
/**
* Instance of the window.requestAnimationFrame call
* @type {?number}
* @protected
*/
this.requestFrameId_ = null;
/**
* Main WebGLRenderer
* @type {THREE.WebGLRenderer}
* @protected
*/
this.renderer_ = null;
/**
* @type {THREE.Clock}
* @protected
*/
this.clock_ = null;
/**
* @type {THREE.Raycaster}
* @protected
*/
this.raycaster_ = null;
/**
* Stats object {@Link https://www.npmjs.com/package/stats-js}
* @type {Stats}
* @protected
*/
this.stats_ = null;
/**
* Defines if the rendering is active
* @type {boolean}
* @protected
*/
this.enabled_ = false;
/**
* Completer is a helper which returns a promise. Its solved if the creation of the 3D scene is completed.
* @type {Completer}
* @protected
*/
this.creationCompleter_ = new Completer();
/**
* Determines whether the application should monitor the framerate to decide if it is necessary
* to create a renderer with different settings with lower quality but better performance.
* @type {boolean}
* @protected
*/
this.checkFrames_ = false;
/**
* Current frame count since rendering was started
* @type {number}
* @protected
*/
this.frames_ = 0;
/**
* Current framerate
* @type {number}
* @protected
*/
this.currentFps_ = 0;
/**
* Used as a value to check the framerate only in a certain time interval.
* @type {number}
* @protected
*/
this.prevTime_ = 0;
/**
* Frame count of lower frames than the defined {@Link Abstract3DWorld#fpsThreshhold_}
* @type {number}
* @protected
*/
this.lowFramerateCount_ = 0;
/**
* Limit of {@Link Abstract3DWorld#lowFramerateCount_} for the current {@Link Abstract3DWorld#lowSettingStep_} of lower setting check
* @type {number|Array<number>}
* @protected
*/
this.lowSettingThreshhold_ = [60, 60];
/**
* Current step of the lower setting reduction
* @type {number}
* @protected
*/
this.lowSettingStep_ = 0;
/**
* General limit or list of limits for the current {@Link Abstract3DWorld#lowSettingStep_} for the check if the current framerate is detected as a lower framerate.
* @type {number|Array<number>}
* @protected
*/
this.fpsThreshhold_ = 20;
/**
* Defines if antialias should be activated for the WebGLRenderer
* @type {boolean}
* @protected
*/
this.antialiasDisabled_ = false;
/**
* Defines if the rendering context should be rendered for retina in double resolution
* @type {number}
* @protected
*/
this.maxPixelRatio_ = this.worldProvider_.isMobileDevice ? 1 : 2;
/**
* Defines whether the 3D scene should be rendered immediately after the component is initialized.
* @type {boolean}
* @protected
*/
this.shouldBuildImmediately_ = true;
/**
* Current camera object which is used in the render process.
* @type {THREE.Camera}
* @protected
*/
this.camera_ = null;
}
/**
* Component is ready and had loaded all dependencies (inherit method waitFor and sub components).
* @inheritDoc
*/
onInit()
{
super.onInit();
let supportsWebGL = this.checkWebGlSupport_();
if(supportsWebGL)
{
if(window['STATS_MODE'] == 'on')
{
this.stats_ = new Stats();
document.body.appendChild(this.stats_.dom);
}
if(this.shouldBuildImmediately_)
{
this.initScene_().then(() => {
this.creationCompleter_.resolve();
});
}
}
else
{
this.displayNoSupportMessage_();
}
}
/**
* Checks if the browser supports WebGL
* @return {boolean}
* @private
*/
checkWebGlSupport_()
{
try {
let canvas = document.createElement('canvas');
return !!(window['WebGLRenderingContext'] && (canvas.getContext('webgl') || canvas.getContext('experimental-webgl')));
} catch (error) {
return false;
}
}
/**
* Display a message if WebGL isn't supported.
* @protected
*/
displayNoSupportMessage_()
{
console.error('WebGL is not supported!');
}
/**
* Can be called to receive a Promise that is triggered when the creation of the 3D scene is completed.
* @return {Promise}
*/
ready()
{
return this.creationCompleter_.getPromise();
}
/**
* Initializes the renderer, creates the raycaster object and starts the creation of the 3D scene.
* @return {Promise}
* @protected
*/
initScene_()
{
if(this.renderer_)
return Promise.resolve();
this.createRenderer_();
if(!this.renderer_)
{
return Promise.reject('Renderer couldn\'t be created.')
}
else
{
this.raycaster_ = new THREE.Raycaster();
return this.buildScene_().then(() => {
listen(this.resizeProvider_, EventType.RESIZE, this.resize_, false, this);
});
}
}
/**
* Creates the main WebGLRenderer
* @protected
*/
createRenderer_()
{
if(this.renderer_)
{
removeAll(this.renderer_.domElement);
this.getElement().removeChild(this.renderer_.domElement);
}
try {
this.renderer_ = new THREE.WebGLRenderer({'antialias': !this.antialiasDisabled_, 'failIfMajorPerformanceCaveat': true});
}
catch(error)
{
console.warn(error);
try {
this.renderer_ = new THREE.WebGLRenderer({'antialias': false, 'failIfMajorPerformanceCaveat': true});
this.antialiasDisabled_ = true;
}
catch(error2) {console.warn(error2);}
}
if(this.renderer_)
{
let size = this.resizeProvider_.getViewportSize();
this.maxPixelRatio_ = window.devicePixelRatio ? Math.min(window.devicePixelRatio, this.maxPixelRatio_) : 1;
this.renderer_.setPixelRatio(this.maxPixelRatio_);
this.renderer_.setSize(size.width, size.height);
this.renderer_.shadowMap.enabled = false;
this.renderer_.shadowMap.type = THREE.PCFSoftShadowMap;
//this.renderer_.autoClear = false;
classlist.add(this.renderer_.domElement, 'three-stage');
this.getElement().appendChild(this.renderer_.domElement);
listen(this.renderer_.domElement, 'webglcontextlost', () => {
console.error("CONTEXT LOST");
}, false, this);
listen(this.renderer_.domElement, 'webglcontextrestored', () => {
console.warn("CONTEXT RESTORED");
}, false, this);
listen(this.worldProvider_, WorldEventType.START_RENDERING, this.startRenderer_, false, this);
listen(this.worldProvider_, WorldEventType.PAUSE_RENDERING, this.pauseRenderer_, false, this);
}
}
/**
* Loads and creates the 3D scene.
* @return {Promise}
* @protected
*/
async buildScene_()
{
return Promise.resolve();
}
/**
* Activates rendering
* @protected
*/
startRenderer_()
{
this.enabled_ = true;
}
/**
* Deactivates rendering
* @protected
*/
pauseRenderer_()
{
this.enabled_ = false;
}
/**
* Enable the active scene for rendering and interaction.
* @protected
*/
activateScene_()
{
if(!this.enabled)
{
this.enabled = true;
this.startRenderScene_();
this.resize_();
this.activateInteraction_();
}
}
/**
* Disable the active scene for rendering and interaction.
* @protected
*/
deactivateScene_()
{
if(this.enabled)
{
this.enabled = false;
this.stopRenderScene_();
this.deactivateInteraction_();
}
}
/**
* Activates interactivity within the 3D scene
* @protected
*/
activateInteraction_()
{
}
/**
* Deactivates interactivity within the 3D scene
* @protected
*/
deactivateInteraction_()
{
}
/**
* Main call for 3D scene updates in the render process.
* @param {number} delta
* @protected
*/
updateScene_(delta)
{
}
/**
* Stops the rendering of the active 3D scene
* @protected
*/
stopRenderScene_()
{
if(this.requestFrameId_ != null)
window.cancelAnimationFrame(this.requestFrameId_);
if(this.clock_)
this.clock_.stop();
this.requestFrameId_ = null;
}
/**
* Starts the rendering of the active 3D scene
* @protected
*/
startRenderScene_()
{
this.stopRenderScene_();
if(!this.clock_)
this.clock_ = new THREE.Clock();
this.clock_.start();
this.drawScene_();
}
/**
*
* @protected
*/
drawScene_()
{
this.renderScene_();
this.requestFrameId_ = window.requestAnimationFrame(this.drawScene_.bind(this));
}
/**
* Main render process (window.requestAnimationFrame call).
* Updates THREE clock, framerate check, stats and 3D scene {@Link Abstract3DWorld#updateScene_}
* @param {boolean=} forcedRendering If it is set to `true`, a rendering will still take place even if {@Link Abstract3DWorld#enabled_} is set to `false`.
* @protected
*/
renderScene_(forcedRendering = false)
{
if(this.renderer_)
{
let delta = this.clock_ ? this.clock_.getDelta() : 1;
if(this.enabled_ || forcedRendering)
{
if (this.stats_)
this.stats_.update();
this.renderer_.clear();
if (this.checkFrames_)
this.checkFramerate_();
this.updateScene_(delta);
}
}
}
/**
* Checks the framerate according to the settings {@Link Abstract3DWorld#lowSettingThreshhold_} and {@Link Abstract3DWorld#fpsThreshhold_}
* for the current {@Link Abstract3DWorld#lowSettingStep_}.
* @protected
*/
checkFramerate_()
{
this.frames_++;
let time = Date.now();
if (time >= this.prevTime_ + 200)
{
this.currentFps_ = (this.frames_ * 1000) / (time - this.prevTime_);
let minFps = typeof this.fpsThreshhold_ == 'number' ? this.fpsThreshhold_ : this.fpsThreshhold_[this.lowSettingStep_];
if (this.currentFps_ < minFps)
{
this.lowFramerateCount_++;
}
this.prevTime_ = time;
this.frames_ = 0;
}
let maxLowFrames = typeof this.lowSettingThreshhold_ == 'number' ? this.lowSettingThreshhold_ : this.lowSettingThreshhold_[this.lowSettingStep_];
if (this.lowFramerateCount_ >= maxLowFrames)
{
this.lowSettingStep_++;
this.lowFramerateCount_ = 0;
this.changeToLowSettings_(this.lowSettingStep_);
this.checkFrames_ = !(typeof this.lowSettingThreshhold_ == 'number' || this.lowSettingStep_ >= this.lowSettingThreshhold_.length);
}
}
/**
* Bad performance has been detected and the WebGLRenderer is being rebuilt with new settings to improve performance.
* @param {number} step Current (already increased) {@Link Abstract3DWorld#lowSettingStep_}
* @protected
*/
changeToLowSettings_(step)
{
console.warn("changeToLowSettings", step);
switch(step) {
case 1: this.antialiasDisabled_ = true;
if(this.maxPixelRatio_ == 1) this.lowSettingStep_ = 2;
this.createRenderer_();
break;
case 2: this.maxPixelRatio_ = 1;
this.createRenderer_();
break;
}
}
/**
* Detects window size changes and therefore updates renderer sizes.
* @protected
*/
resize_()
{
if(!this.renderer_)
return;
let size = this.resizeProvider_.getViewportSize();
setStyle(this.renderer_.domElement, {
'width': size.width + "px",
'height': size.height + "px"
});
this.renderer_.domElement.width = size.width;
this.renderer_.domElement.height = size.height;
this.renderer_.setSize(size.width, size.height);
this.updateCameraMatrix_();
}
/**
* Updates the current camera aspect based on the window size.
* @protected
*/
updateCameraMatrix_()
{
if(this.camera_)
{
let size = this.resizeProvider_.getViewportSize();
this.camera_.aspect = size.width / size.height;
this.camera_.updateProjectionMatrix();
}
}
/**
* Setter
* @param {boolean} value
*/
set enabled(value)
{
this.enabled_ = value;
}
/**
* Getter
* @return {boolean}
*/
get enabled()
{
return this.enabled_;
}
}
exports = Abstract3DWorld;