const THREE = require('three'); const ResizeSensor = require('css-element-queries/src/ResizeSensor'); /** * Create a Zinc 3D renderer in the container provided. * The primary function of a Zinc 3D renderer is to display the current * scene (@link Scene} set to the renderer and each scene may contain as * many geometries, glyphset and other primitives as the system can support. * Zinc.Renderer also allows additional scenes to be displayed. * * @param {Object} containerIn - Container to create the renderer on. * @class * @author Alan Wu * @return {Renderer} */ exports.Renderer = function (containerIn) { let container = containerIn; const stats = 0; let renderer = undefined; let currentScene = undefined; //myGezincGeometriestains a tuple of the threejs mesh, timeEnabled, morphColour flag, unique id and morph const clock = new THREE.Clock(false); this.playAnimation = true; /* default animation update rate, rate is 1000 and duration is default to 6000, 6s to finish a full animation */ let playRate = 1000; let preRenderCallbackFunctions = []; let preRenderCallbackFunctions_id = 0; let postRenderCallbackFunctions = []; let postRenderCallbackFunctions_id = 0; let animated_id = undefined; let cameraOrtho = undefined, sceneOrtho = undefined, logoSprite = undefined; let sceneMap = []; let additionalActiveScenes = []; let scenesGroup = new THREE.Group(); let canvas = undefined; let sensor = undefined; let isRendering = false; const _this = this; const currentSize = [0, 0]; const currentOffset = [0, 0]; this.getDrawingWidth = () => { if (container) { return container.clientWidth; } else if (canvas) if (typeof canvas.clientWidth !== 'undefined') return Math.round(canvas.clientWidth); else return Math.round(canvas.width); return 0; } this.getDrawingHeight = () => { if (container) { return container.clientHeight; } else if (canvas) if (typeof canvas.clientHeight !== 'undefined') return Math.round(canvas.clientHeight); else return Math.round(canvas.height); return 0; } /** * Call this to resize the renderer, this is normally call automatically. */ this.onWindowResize = () => { currentScene.onWindowResize(); const width = this.getDrawingWidth(); const height = this.getDrawingHeight(); if (renderer != undefined) { let localRect = undefined; if (container) { localRect = container.getBoundingClientRect(); renderer.setSize(width, height); } else if (canvas) { if (typeof canvas.getBoundingClientRect !== 'undefined') { localRect = canvas.getBoundingClientRect(); canvas.width = width; canvas.height = height; renderer.setSize(width, height, false); } else { renderer.setSize(width, height, false); } } if (localRect) { currentOffset[0] = localRect.left; currentOffset[1] = localRect.top; } const target = new THREE.Vector2(); renderer.getSize(target); currentSize[0] = target.x; currentSize[1] = target.y; } } /** * Initialise the renderer and its visualisations. */ this.initialiseVisualisation = parameters => { parameters = parameters || {}; if (parameters['antialias'] === undefined) { let onMobile = false; try { if( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ) { onMobile = true; } } catch(err) { onMobile = false; } if (onMobile) parameters['antialias'] = false; else parameters['antialias'] = true; } if (parameters["canvas"]) { container = undefined; canvas = parameters["canvas"]; } renderer = new THREE.WebGLRenderer(parameters); if (container !== undefined) { container.appendChild( renderer.domElement ); } renderer.setClearColor( 0xffffff, 1); if (canvas && canvas.style) { canvas.style.height = "100%"; canvas.style.width = "100%"; } renderer.autoClear = false; const scene = this.createScene("default"); this.setCurrentScene(scene); } /** * Get the current scene on display. * @return {Zinc.Scene}; */ this.getCurrentScene = () => { return currentScene; } /** * Set the current scene on display. * * @param {Zinc.Scene} sceneIn - The scene to be set, only scene created by this instance * of ZincRenderer is supported currently. */ this.setCurrentScene = sceneIn => { if (sceneIn) { this.removeActiveScene(sceneIn); const oldScene = currentScene; currentScene = sceneIn; if (oldScene) { oldScene.setInteractiveControlEnable(false); } currentScene.setInteractiveControlEnable(true); currentScene.setAdditionalScenesGroup(scenesGroup); this.onWindowResize(); } } /** * Return scene with the matching name if scene with that name has been created. * * @param {String} name - Name to match * @return {Zinc.Scene} */ this.getSceneByName = name => { return sceneMap[name]; } /** * Create a new scene with the provided name if scene with the same name exists, * return undefined. * * @param {String} name - Name of the scene to be created. * @return {Zinc.Scene} */ this.createScene = name => { if (sceneMap[name] != undefined){ return undefined; } else { let new_scene = undefined; if (canvas) new_scene = new (require('./scene').Scene)(canvas, renderer); else new_scene = new (require('./scene').Scene)(container, renderer); sceneMap[name] = new_scene; new_scene.sceneName = name; return new_scene; } } const updateOrthoScene = () => { if (logoSprite != undefined) { const material = logoSprite.material; if (material.map) { const width = this.getDrawingWidth(); const height = this.getDrawingHeight(); const calculatedWidth = (width - material.map.image.width)/2; const calculatedHeight = (-height + material.map.image.height)/2; logoSprite.position.set(calculatedWidth, calculatedHeight, 1 ); } } }; const updateOrthoCamera = () => { if (cameraOrtho != undefined) { const width = this.getDrawingWidth(); const height = this.getDrawingHeight(); cameraOrtho.left = -width / 2; cameraOrtho.right = width / 2; cameraOrtho.top = height / 2; cameraOrtho.bottom = -height / 2; cameraOrtho.updateProjectionMatrix(); } }; /** * Reset the viewport of the current scene to its original state. */ this.resetView = () => { currentScene.resetView(); } /** * Adjust zoom distance to include all primitives in scene and also the additional scenes * but the lookat direction and up vectors will remain constant. */ this.viewAll = () => { if (currentScene) { const boundingBox = currentScene.getBoundingBox(); if (boundingBox) { for(let i = 0; i < additionalActiveScenes.length; i++) { const boundingBox2 = additionalActiveScenes[i].getBoundingBox(); if (boundingBox2) { boundingBox.union(boundingBox2); } } currentScene.viewAllWithBoundingBox(boundingBox); } } } /** * Load a legacy model(s) format with the provided URLs and parameters. This only loads the geometry * without any of the metadata. Therefore, extra parameters should be provided. This should be * called from {@link Zinc.Scene}. * * @deprecated */ this.loadModelsURL = (urls, colours, opacities, timeEnabled, morphColour, finishCallback) => { currentScene.loadModelsURL(urls, colours, opacities, timeEnabled, morphColour, finishCallback); } const loadView = viewData => { currentScene.loadView(viewData); }; /** * Load the viewport from an external location provided by the url. This should be * called from {@link Zinc.Scene}; * @param {String} URL - address to the file containing viewport information. * @deprecated */ this.loadViewURL = url => { currentScene.loadViewURL(url); } /** * Load a legacy file format containing the viewport and its model file from an external * location provided by the url. Use the new metadata format with * {@link Zinc.Scene#loadMetadataURL} instead. This should be * called from {@link Zinc.Scene}; * * @param {String} URL - address to the file containing viewport and model information. * @deprecated */ this.loadFromViewURL = (jsonFilePrefix, finishCallback) => { currentScene.loadFromViewURL(jsonFilePrefix, finishCallback); } this.updateDirectionalLight = () => { currentScene.updateDirectionalLight(); } let runAnimation = () => { if (isRendering) { animated_id = requestAnimationFrame( runAnimation ); this.render(); } else { cancelAnimationFrame(animated_id); animated_id = undefined; } } /** * Stop the animation and renderer to get into the render loop. */ this.stopAnimate = () => { if (isRendering) { clock.stop(); isRendering = false; } } /** * Start the animation and begin the rendering loop. */ this.animate = () => { if (!isRendering) { clock.start(); isRendering = true; runAnimation(); } } const prevTime = Date.now(); /** * Add a callback function which will be called everytime before the renderer renders its scene. * @param {Function} callbackFunction - callbackFunction to be added. * * @return {Number} */ this.addPreRenderCallbackFunction = callbackFunction => { preRenderCallbackFunctions_id = preRenderCallbackFunctions_id + 1; preRenderCallbackFunctions[preRenderCallbackFunctions_id] = callbackFunction; return preRenderCallbackFunctions_id; } /** * Remove a callback function that is previously added to the scene. * @param {Number} id - identifier of the previously added callback function. */ this.removePreRenderCallbackFunction = id => { if (id in preRenderCallbackFunctions) { delete preRenderCallbackFunctions[id]; } } /** * Add a callback function which will be called everytime after the renderer renders its scene. * @param {Function} callbackFunction - callbackFunction to be added. * * @return {Number} */ this.addPostRenderCallbackFunction = callbackFunction => { postRenderCallbackFunctions_id = postRenderCallbackFunctions_id + 1; postRenderCallbackFunctions[postRenderCallbackFunctions_id] = callbackFunction; return postRenderCallbackFunctions_id; } /** * Remove a callback function that is previously added to the scene. * @param {Number} id - identifier of the previously added callback function. */ this.removePostRenderCallbackFunction = id => { if (id in postRenderCallbackFunctions) { delete postRenderCallbackFunctions[id]; } } /** * Get the current play rate, playrate affects how fast an animated object animates. * Also see {@link Zinc.Scene#duration}. */ this.getPlayRate = () => { return playRate; } /** * Set the current play rate, playrate affects how fast an animated object animates. * @param {Number} PlayRateIn - value to set the playrate to. * Also see {@link Zinc.Scene#duration}. */ this.setPlayRate = playRateIn => { playRate = playRateIn; } this.getCurrentTime = () => { return currentScene.getCurrentTime(); } /** * Get the current play rate, playrate affects how fast an animated object animates. * Also see {@link Zinc.Scene#duration}. */ this.setMorphsTime = time => { currentScene.setMorphsTime(time); } /** * Get {Zinc.Geoemtry} by its id. This should be called from {@link Zinc.Scene}; * * @depreacted * @return {Zinc.Geometry} */ this.getZincGeometryByID = id => { return currentScene.getZincGeometryByID(id); } /** * Add {Three.Object} to the current scene. */ this.addToScene = object => { currentScene.addObject(object) } /** * Add {Three.Object} to the ortho scene, objects added to the ortho scene are rendered in * normalised coordinates and overlay on top of current scene. * */ this.addToOrthoScene = object => { if (sceneOrtho == undefined) sceneOrtho = new THREE.Scene(); if (cameraOrtho == undefined) { const width = this.getDrawingWidth(); const height = this.getDrawingHeight(); cameraOrtho = new THREE.OrthographicCamera( -width / 2, width / 2, height/ 2, -height / 2, 1, 10 ); cameraOrtho.position.z = 10; } sceneOrtho.add(object) } const createHUDSprites = logoSprite => { return texture => { texture.needsUpdate = true; const material = new THREE.SpriteMaterial( { map: texture } ); const imagewidth = material.map.image.width; const imageheight = material.map.image.height; logoSprite.material = material; logoSprite.scale.set( imagewidth, imageheight, 1 ); const width = this.getDrawingWidth(); const height = this.getDrawingHeight(); logoSprite.position.set( (width - imagewidth)/2, (-height + imageheight)/2, 1 ); this.addToOrthoScene(logoSprite); }; }; this.addLogo = () => { logoSprite = new THREE.Sprite(); const logo = THREE.ImageUtils.loadTexture( "images/abi_big_logo_transparent_small.png", undefined, createHUDSprites(logoSprite)); } /** * Render the current and all additional scenes. It will first update all geometries and glyphsets * in scenes, clear depth buffer and render the ortho scene, call the preRenderCallbackFunctions stack * , render the scenes then postRenderCallback. */ this.render = () => { if (!sensor) { if (container) { if (container.clientWidth > 0 && container.clientHeight > 0) sensor = new ResizeSensor(container, this.onWindowResize); } else if (canvas) { if (canvas.width > 0 && canvas.height > 0) sensor = new ResizeSensor(canvas, this.onWindowResize); } } const delta = clock.getDelta(); currentScene.renderGeometries(playRate, delta, this.playAnimation); for(let i = 0; i < additionalActiveScenes.length; i++) { const sceneItem = additionalActiveScenes[i]; sceneItem.renderGeometries(playRate, delta, this.playAnimation); } if (cameraOrtho != undefined && sceneOrtho != undefined) { renderer.clearDepth(); renderer.render( sceneOrtho, cameraOrtho ); } for (let key in preRenderCallbackFunctions) { if (preRenderCallbackFunctions.hasOwnProperty(key)) { preRenderCallbackFunctions[key].call(); } } currentScene.render(renderer); for (let key in postRenderCallbackFunctions) { if (postRenderCallbackFunctions.hasOwnProperty(key)) { postRenderCallbackFunctions[key].call(); } } } /** * Get the internal {@link Three.Renderer}, to gain access to ThreeJS APIs. */ this.getThreeJSRenderer = () => { return renderer; } /** * Check if a scene is currently active. * @param {Zinc.Scene} sceneIn - Scene to check if it is currently * rendered. */ this.isSceneActive = sceneIn => { if (currentScene === sceneIn) { return true; } else { for(let i = 0; i < additionalActiveScenes.length; i++) { const sceneItem = additionalActiveScenes[i]; if (sceneItem === sceneIn) return true; } } return false; } /** * Add additional active scene for rendering, this scene will also be rendered but * viewport of the currentScene will be used. * @param {Zinc.Scene} additionalScene - Scene to be added to the rendering. */ this.addActiveScene = additionalScene => { if (!this.isSceneActive(additionalScene)) { additionalActiveScenes.push(additionalScene); scenesGroup.add(additionalScene.getThreeJSScene()); } } /** * Remove a currenrtly active scene from the renderer, this scene will also be rendered but * viewport of the currentScene will be used. * @param {Zinc.Scene} additionalScene - Scene to be removed from rendering. */ this.removeActiveScene = additionalScene => { for(let i = 0; i < additionalActiveScenes.length; i++) { const sceneItem = additionalActiveScenes[i]; if (sceneItem === additionalScene) { additionalActiveScenes.splice(i, 1); scenesGroup.remove(additionalScene.getThreeJSScene()); return; } } } /** * Clear all additional scenes from rendering except for curentScene. */ this.clearAllActiveScene = () => { for (let i = 0; i < additionalActiveScenes.length; i++) { scenesGroup.remove(additionalActiveScenes[i].getThreeJSScene()); } additionalActiveScenes.splice(0,additionalActiveScenes.length); } /** * Dispose all memory allocated, this will effetively destroy all scenes. */ this.dispose = () => { if (isRendering) cancelAnimationFrame(animated_id); for (const key in sceneMap) { if (sceneMap.hasOwnProperty(key)) { sceneMap[key].clearAll(); } } sceneMap = []; additionalActiveScenes = []; scenesGroup = new THREE.Group(); this.stopAnimate(); preRenderCallbackFunctions = []; preRenderCallbackFunctions_id = 0; cameraOrtho = undefined; sceneOrtho = undefined; logoSprite = undefined; const scene = this.createScene("default"); this.setCurrentScene(scene); sensor = undefined; } /** * Transition from the current viewport to the endingScene's viewport in the specified duration. * * @param {Zinc.Scene} endingScene - Viewport of this scene will be used as the destination. * @param {Number} duration - Amount of time to transition from current viewport to the * endingScene's viewport. */ this.transitionScene = (endingScene, duration) => { if (currentScene) { const currentCamera = currentScene.getZincCameraControls(); const boundingBox = endingScene.getBoundingBox(); if (boundingBox) { const radius = boundingBox.min.distanceTo(boundingBox.max)/2.0; const centreX = (boundingBox.min.x + boundingBox.max.x) / 2.0; const centreY = (boundingBox.min.y + boundingBox.max.y) / 2.0; const centreZ = (boundingBox.min.z + boundingBox.max.z) / 2.0; const clip_factor = 4.0; const endingViewport = currentCamera.getViewportFromCentreAndRadius(centreX, centreY, centreZ, radius, 40, radius * clip_factor ); const startingViewport = currentCamera.getCurrentViewport(); currentCamera.cameraTransition(startingViewport, endingViewport, duration); currentCamera.enableCameraTransition(); } } } this.isWebGL2 = () => { if (renderer) return renderer.capabilities.isWebGL2; return false; } };