viewer.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. import { viewerManager } from './viewerManager';
  2. import { TemplateManager } from './../templateManager';
  3. import configurationLoader from './../configuration/loader';
  4. import { CubeTexture, Color3, IEnvironmentHelperOptions, EnvironmentHelper, Effect, SceneOptimizer, SceneOptimizerOptions, Observable, Engine, Scene, ArcRotateCamera, Vector3, SceneLoader, AbstractMesh, Mesh, HemisphericLight, Database, SceneLoaderProgressEvent, ISceneLoaderPlugin, ISceneLoaderPluginAsync } from 'babylonjs';
  5. import { ViewerConfiguration } from '../configuration/configuration';
  6. import { PromiseObservable } from '../util/promiseObservable';
  7. export abstract class AbstractViewer {
  8. public templateManager: TemplateManager;
  9. public engine: Engine;
  10. public scene: Scene;
  11. public sceneOptimizer: SceneOptimizer;
  12. public baseId: string;
  13. /**
  14. * The last loader used to load a model.
  15. *
  16. * @type {(ISceneLoaderPlugin | ISceneLoaderPluginAsync)}
  17. * @memberof AbstractViewer
  18. */
  19. public lastUsedLoader: ISceneLoaderPlugin | ISceneLoaderPluginAsync;
  20. protected configuration: ViewerConfiguration;
  21. protected environmentHelper: EnvironmentHelper;
  22. protected defaultHighpTextureType: number;
  23. protected shadowGeneratorBias: number;
  24. protected defaultPipelineTextureType: number;
  25. protected maxShadows: number;
  26. // observables
  27. public onSceneInitObservable: PromiseObservable<Scene>;
  28. public onEngineInitObservable: PromiseObservable<Engine>;
  29. public onModelLoadedObservable: PromiseObservable<AbstractMesh[]>;
  30. public onModelLoadProgressObservable: PromiseObservable<SceneLoaderProgressEvent>;
  31. public onInitDoneObservable: PromiseObservable<AbstractViewer>;
  32. protected canvas: HTMLCanvasElement;
  33. constructor(public containerElement: HTMLElement, initialConfiguration: ViewerConfiguration = {}) {
  34. // if exists, use the container id. otherwise, generate a random string.
  35. if (containerElement.id) {
  36. this.baseId = containerElement.id;
  37. } else {
  38. this.baseId = containerElement.id = 'bjs' + Math.random().toString(32).substr(2, 8);
  39. }
  40. this.onSceneInitObservable = new PromiseObservable();
  41. this.onEngineInitObservable = new PromiseObservable();
  42. this.onModelLoadedObservable = new PromiseObservable();
  43. this.onModelLoadProgressObservable = new PromiseObservable();
  44. this.onInitDoneObservable = new PromiseObservable();
  45. // add this viewer to the viewer manager
  46. viewerManager.addViewer(this);
  47. // create a new template manager. TODO - singleton?
  48. this.templateManager = new TemplateManager(containerElement);
  49. this.prepareContainerElement();
  50. // extend the configuration
  51. configurationLoader.loadConfiguration(initialConfiguration).then((configuration) => {
  52. this.configuration = configuration;
  53. // adding preconfigured functions
  54. if (this.configuration.observers) {
  55. if (this.configuration.observers.onEngineInit) {
  56. this.onEngineInitObservable.add(window[this.configuration.observers.onEngineInit]);
  57. }
  58. if (this.configuration.observers.onSceneInit) {
  59. this.onSceneInitObservable.add(window[this.configuration.observers.onSceneInit]);
  60. }
  61. if (this.configuration.observers.onModelLoaded) {
  62. this.onModelLoadedObservable.add(window[this.configuration.observers.onModelLoaded]);
  63. }
  64. }
  65. // initialize the templates
  66. let templateConfiguration = this.configuration.templates || {};
  67. this.templateManager.initTemplate(templateConfiguration);
  68. // when done, execute onTemplatesLoaded()
  69. this.templateManager.onAllLoaded.add(() => {
  70. let canvas = this.templateManager.getCanvas();
  71. if (canvas) {
  72. this.canvas = canvas;
  73. }
  74. this._onTemplateLoaded();
  75. });
  76. });
  77. //this.onModelLoadedObservable.add(this.initEnvironment.bind(this));
  78. }
  79. public getBaseId(): string {
  80. return this.baseId;
  81. }
  82. public isCanvasInDOM(): boolean {
  83. return !!this.canvas && !!this.canvas.parentElement;
  84. }
  85. protected resize = (): void => {
  86. // Only resize if Canvas is in the DOM
  87. if (!this.isCanvasInDOM()) {
  88. return;
  89. }
  90. if (this.canvas.clientWidth <= 0 || this.canvas.clientHeight <= 0) {
  91. return;
  92. }
  93. this.engine.resize();
  94. }
  95. protected render = (): void => {
  96. this.scene && this.scene.render();
  97. }
  98. public dispose() {
  99. window.removeEventListener('resize', this.resize);
  100. this.sceneOptimizer.stop();
  101. this.sceneOptimizer.dispose();
  102. if (this.scene.activeCamera) {
  103. this.scene.activeCamera.detachControl(this.canvas);
  104. }
  105. this.scene.dispose();
  106. this.engine.dispose();
  107. this.templateManager.dispose();
  108. }
  109. protected abstract prepareContainerElement();
  110. /**
  111. * This function will execute when the HTML templates finished initializing.
  112. * It should initialize the engine and continue execution.
  113. *
  114. * @protected
  115. * @returns {Promise<AbstractViewer>} The viewer object will be returned after the object was loaded.
  116. * @memberof AbstractViewer
  117. */
  118. protected onTemplatesLoaded(): Promise<AbstractViewer> {
  119. return Promise.resolve(this);
  120. }
  121. /**
  122. * This will force the creation of an engine and a scene.
  123. * It will also load a model if preconfigured.
  124. * But first - it will load the extendible onTemplateLoaded()!
  125. */
  126. private _onTemplateLoaded(): Promise<AbstractViewer> {
  127. return this.onTemplatesLoaded().then(() => {
  128. let autoLoadModel = !!this.configuration.model;
  129. return this.initEngine().then((engine) => {
  130. return this.onEngineInitObservable.notifyWithPromise(engine);
  131. }).then(() => {
  132. if (autoLoadModel) {
  133. return this.loadModel();
  134. } else {
  135. return this.scene || this.initScene();
  136. }
  137. }).then((scene) => {
  138. return this.onSceneInitObservable.notifyWithPromise(scene);
  139. }).then(() => {
  140. return this.onInitDoneObservable.notifyWithPromise(this);
  141. }).then(() => {
  142. return this;
  143. });
  144. })
  145. }
  146. /**
  147. * Initialize the engine. Retruns a promise in case async calls are needed.
  148. *
  149. * @protected
  150. * @returns {Promise<Engine>}
  151. * @memberof Viewer
  152. */
  153. protected initEngine(): Promise<Engine> {
  154. // init custom shaders
  155. this.injectCustomShaders();
  156. let canvasElement = this.templateManager.getCanvas();
  157. if (!canvasElement) {
  158. return Promise.reject('Canvas element not found!');
  159. }
  160. let config = this.configuration.engine || {};
  161. // TDO enable further configuration
  162. this.engine = new Engine(canvasElement, !!config.antialiasing, config.engineOptions);
  163. // Disable manifest checking
  164. Database.IDBStorageEnabled = false;
  165. if (!config.disableResize) {
  166. window.addEventListener('resize', this.resize);
  167. }
  168. this.engine.runRenderLoop(this.render);
  169. if (this.configuration.engine && this.configuration.engine.adaptiveQuality) {
  170. var scale = Math.max(0.5, 1 / (window.devicePixelRatio || 2));
  171. this.engine.setHardwareScalingLevel(scale);
  172. }
  173. // set hardware limitations for scene initialization
  174. this.handleHardwareLimitations();
  175. return Promise.resolve(this.engine);
  176. }
  177. protected initScene(): Promise<Scene> {
  178. // if the scen exists, dispose it.
  179. if (this.scene) {
  180. this.scene.dispose();
  181. }
  182. // create a new scene
  183. this.scene = new Scene(this.engine);
  184. // make sure there is a default camera and light.
  185. this.scene.createDefaultCameraOrLight(true, true, true);
  186. if (this.configuration.scene) {
  187. if (this.configuration.scene.debug) {
  188. this.scene.debugLayer.show();
  189. }
  190. // Scene optimizer
  191. if (this.configuration.optimizer) {
  192. let optimizerConfig = this.configuration.optimizer;
  193. let optimizerOptions: SceneOptimizerOptions = new SceneOptimizerOptions(optimizerConfig.targetFrameRate, optimizerConfig.trackerDuration);
  194. // check for degradation
  195. if (optimizerConfig.degradation) {
  196. switch (optimizerConfig.degradation) {
  197. case "low":
  198. optimizerOptions = SceneOptimizerOptions.LowDegradationAllowed(optimizerConfig.targetFrameRate);
  199. break;
  200. case "moderate":
  201. optimizerOptions = SceneOptimizerOptions.ModerateDegradationAllowed(optimizerConfig.targetFrameRate);
  202. break;
  203. case "hight":
  204. optimizerOptions = SceneOptimizerOptions.HighDegradationAllowed(optimizerConfig.targetFrameRate);
  205. break;
  206. }
  207. }
  208. this.sceneOptimizer = new SceneOptimizer(this.scene, optimizerOptions, optimizerConfig.autoGeneratePriorities, optimizerConfig.improvementMode);
  209. this.sceneOptimizer.start();
  210. }
  211. // image processing configuration - optional.
  212. if (this.configuration.scene.imageProcessingConfiguration) {
  213. this.extendClassWithConfig(this.scene.imageProcessingConfiguration, this.configuration.scene.imageProcessingConfiguration);
  214. }
  215. if (this.configuration.scene.environmentTexture) {
  216. const environmentTexture = CubeTexture.CreateFromPrefilteredData(this.configuration.scene.environmentTexture, this.scene);
  217. this.scene.environmentTexture = environmentTexture;
  218. }
  219. }
  220. return Promise.resolve(this.scene);
  221. }
  222. public loadModel(model: any = this.configuration.model, clearScene: boolean = true): Promise<Scene> {
  223. this.configuration.model = model;
  224. let modelUrl = (typeof model === 'string') ? model : model.url;
  225. let parts = modelUrl.split('/');
  226. let filename = parts.pop();
  227. let base = parts.join('/') + '/';
  228. let plugin = (typeof model === 'string') ? undefined : model.loader;
  229. return Promise.resolve(this.scene).then((scene) => {
  230. if (!scene || clearScene) return this.initScene();
  231. else return this.scene!;
  232. }).then(() => {
  233. return new Promise<Array<AbstractMesh>>((resolve, reject) => {
  234. this.lastUsedLoader = SceneLoader.ImportMesh(undefined, base, filename, this.scene, (meshes) => {
  235. resolve(meshes);
  236. }, (progressEvent) => {
  237. this.onModelLoadProgressObservable.notifyWithPromise(progressEvent);
  238. }, (e, m, exception) => {
  239. // console.log(m, exception);
  240. reject(m);
  241. }, plugin)!;
  242. });
  243. }).then((meshes: Array<AbstractMesh>) => {
  244. return this.onModelLoadedObservable.notifyWithPromise(meshes)
  245. .then(() => {
  246. this.initEnvironment();
  247. }).then(() => {
  248. return this.scene;
  249. });
  250. });
  251. }
  252. protected initEnvironment(focusMeshes: Array<AbstractMesh> = []): Promise<Scene> {
  253. if (!this.configuration.skybox && !this.configuration.ground) {
  254. if (this.environmentHelper) {
  255. this.environmentHelper.dispose();
  256. };
  257. return Promise.resolve(this.scene);
  258. }
  259. const options: Partial<IEnvironmentHelperOptions> = {
  260. createGround: !!this.configuration.ground,
  261. createSkybox: !!this.configuration.skybox,
  262. setupImageProcessing: false // will be done at the scene level!
  263. };
  264. if (this.configuration.ground) {
  265. let groundConfig = (typeof this.configuration.ground === 'boolean') ? {} : this.configuration.ground;
  266. let groundSize = groundConfig.size || (this.configuration.skybox && this.configuration.skybox.scale);
  267. if (groundSize) {
  268. options.groundSize = groundSize;
  269. }
  270. options.enableGroundShadow = this.configuration.ground === true || groundConfig.receiveShadows;
  271. if (groundConfig.shadowLevel) {
  272. options.groundShadowLevel = groundConfig.shadowLevel;
  273. }
  274. options.enableGroundMirror = !!groundConfig.mirror;
  275. if (groundConfig.texture) {
  276. options.groundTexture = groundConfig.texture;
  277. }
  278. if (groundConfig.color) {
  279. options.groundColor = new Color3(groundConfig.color.r, groundConfig.color.g, groundConfig.color.b)
  280. }
  281. if (groundConfig.mirror) {
  282. options.enableGroundMirror = true;
  283. // to prevent undefines
  284. if (typeof groundConfig.mirror === "object") {
  285. if (groundConfig.mirror.amount)
  286. options.groundMirrorAmount = groundConfig.mirror.amount;
  287. if (groundConfig.mirror.sizeRatio)
  288. options.groundMirrorSizeRatio = groundConfig.mirror.sizeRatio;
  289. if (groundConfig.mirror.blurKernel)
  290. options.groundMirrorBlurKernel = groundConfig.mirror.blurKernel;
  291. if (groundConfig.mirror.fresnelWeight)
  292. options.groundMirrorFresnelWeight = groundConfig.mirror.fresnelWeight;
  293. if (groundConfig.mirror.fallOffDistance)
  294. options.groundMirrorFallOffDistance = groundConfig.mirror.fallOffDistance;
  295. if (this.defaultHighpTextureType !== undefined)
  296. options.groundMirrorTextureType = this.defaultHighpTextureType;
  297. }
  298. }
  299. }
  300. let postInitSkyboxMaterial = false;
  301. if (this.configuration.skybox) {
  302. let conf = this.configuration.skybox;
  303. if (conf.material && conf.material.imageProcessingConfiguration) {
  304. options.setupImageProcessing = false; // will be configured later manually.
  305. }
  306. let skyboxSize = this.configuration.skybox.scale;
  307. if (skyboxSize) {
  308. options.skyboxSize = skyboxSize;
  309. }
  310. options.sizeAuto = !options.skyboxSize;
  311. if (conf.color) {
  312. options.skyboxColor = new Color3(conf.color.r, conf.color.g, conf.color.b)
  313. }
  314. if (conf.cubeTexture && conf.cubeTexture.url) {
  315. if (typeof conf.cubeTexture.url === "string") {
  316. options.skyboxTexture = conf.cubeTexture.url;
  317. } else {
  318. // init later!
  319. postInitSkyboxMaterial = true;
  320. }
  321. }
  322. if (conf.material && conf.material.imageProcessingConfiguration) {
  323. postInitSkyboxMaterial = true;
  324. }
  325. }
  326. if (!this.environmentHelper) {
  327. this.environmentHelper = this.scene.createDefaultEnvironment(options)!;
  328. }
  329. else {
  330. // there might be a new scene! we need to dispose.
  331. // Need to decide if a scene should stay or be disposed.
  332. this.environmentHelper.dispose();
  333. //this.environmentHelper.updateOptions(options);
  334. this.environmentHelper = this.scene.createDefaultEnvironment(options)!;
  335. }
  336. console.log(options);
  337. if (postInitSkyboxMaterial) {
  338. let skyboxMaterial = this.environmentHelper.skyboxMaterial;
  339. if (skyboxMaterial) {
  340. if (this.configuration.skybox && this.configuration.skybox.material && this.configuration.skybox.material.imageProcessingConfiguration) {
  341. this.extendClassWithConfig(skyboxMaterial.imageProcessingConfiguration, this.configuration.skybox.material.imageProcessingConfiguration);
  342. }
  343. }
  344. }
  345. return Promise.resolve(this.scene);
  346. }
  347. /**
  348. * Alters render settings to reduce features based on hardware feature limitations
  349. * @param options Viewer options to modify
  350. */
  351. protected handleHardwareLimitations() {
  352. //flip rendering settings switches based on hardware support
  353. let maxVaryingRows = this.engine.getCaps().maxVaryingVectors;
  354. let maxFragmentSamplers = this.engine.getCaps().maxTexturesImageUnits;
  355. //shadows are disabled if there's not enough varyings for a single shadow
  356. if ((maxVaryingRows < 8) || (maxFragmentSamplers < 8)) {
  357. this.maxShadows = 0;
  358. } else {
  359. this.maxShadows = 3;
  360. }
  361. //can we render to any >= 16-bit targets (required for HDR)
  362. let caps = this.engine.getCaps();
  363. let linearHalfFloatTargets = caps.textureHalfFloatRender && caps.textureHalfFloatLinearFiltering;
  364. let linearFloatTargets = caps.textureFloatRender && caps.textureFloatLinearFiltering;
  365. let supportsHDR: boolean = !!(linearFloatTargets || linearHalfFloatTargets);
  366. if (linearHalfFloatTargets) {
  367. this.defaultHighpTextureType = Engine.TEXTURETYPE_HALF_FLOAT;
  368. this.shadowGeneratorBias = 0.002;
  369. } else if (linearFloatTargets) {
  370. this.defaultHighpTextureType = Engine.TEXTURETYPE_FLOAT;
  371. this.shadowGeneratorBias = 0.001;
  372. } else {
  373. this.defaultHighpTextureType = Engine.TEXTURETYPE_UNSIGNED_INT;
  374. this.shadowGeneratorBias = 0.001;
  375. }
  376. this.defaultPipelineTextureType = supportsHDR ? this.defaultHighpTextureType : Engine.TEXTURETYPE_UNSIGNED_INT;
  377. }
  378. /**
  379. * Injects all the spectre shader in the babylon shader store
  380. */
  381. protected injectCustomShaders(): void {
  382. let customShaders = this.configuration.customShaders;
  383. // Inject all the spectre shader in the babylon shader store.
  384. if (!customShaders) {
  385. return;
  386. }
  387. if (customShaders.shaders) {
  388. Object.keys(customShaders.shaders).forEach(key => {
  389. // typescript considers a callback "unsafe", so... '!'
  390. Effect.ShadersStore[key] = customShaders!.shaders![key];
  391. });
  392. }
  393. if (customShaders.includes) {
  394. Object.keys(customShaders.includes).forEach(key => {
  395. // typescript considers a callback "unsafe", so... '!'
  396. Effect.IncludesShadersStore[key] = customShaders!.includes![key];
  397. });
  398. }
  399. }
  400. protected extendClassWithConfig(object: any, config: any) {
  401. if (!config) return;
  402. Object.keys(config).forEach(key => {
  403. if (key in object && typeof object[key] !== 'function') {
  404. if (typeof object[key] === 'function') return;
  405. // if it is an object, iterate internally until reaching basic types
  406. if (typeof object[key] === 'object') {
  407. this.extendClassWithConfig(object[key], config[key]);
  408. } else {
  409. object[key] = config[key];
  410. }
  411. }
  412. });
  413. }
  414. }