UNPKG

itowns

Version:

A JS/WebGL framework for 3D geospatial data visualization

209 lines (197 loc) 8.14 kB
import { EventDispatcher } from 'three'; export const RENDERING_PAUSED = 0; export const RENDERING_SCHEDULED = 1; /** * MainLoop's update events list that are fired using * {@link View#execFrameRequesters}. * * @property UPDATE_START {string} fired at the start of the update * @property BEFORE_CAMERA_UPDATE {string} fired before the camera update * @property AFTER_CAMERA_UPDATE {string} fired after the camera update * @property BEFORE_LAYER_UPDATE {string} fired before the layer update * @property AFTER_LAYER_UPDATE {string} fired after the layer update * @property BEFORE_RENDER {string} fired before the render * @property AFTER_RENDER {string} fired after the render * @property UPDATE_END {string} fired at the end of the update */ export const MAIN_LOOP_EVENTS = { UPDATE_START: 'update_start', BEFORE_CAMERA_UPDATE: 'before_camera_update', AFTER_CAMERA_UPDATE: 'after_camera_update', BEFORE_LAYER_UPDATE: 'before_layer_update', AFTER_LAYER_UPDATE: 'after_layer_update', BEFORE_RENDER: 'before_render', AFTER_RENDER: 'after_render', UPDATE_END: 'update_end' }; function updateElements(context, geometryLayer, elements) { if (!elements) { return; } for (const element of elements) { // update element // TODO find a way to notify attachedLayers when geometryLayer deletes some elements // and then update Debug.js:addGeometryLayerDebugFeatures const newElementsToUpdate = geometryLayer.update(context, geometryLayer, element); const sub = geometryLayer.getObjectToUpdateForAttachedLayers(element); if (sub) { if (sub.element) { // update attached layers for (const attachedLayer of geometryLayer.attachedLayers) { if (attachedLayer.ready) { attachedLayer.update(context, attachedLayer, sub.element, sub.parent); } } } else if (sub.elements) { for (let i = 0; i < sub.elements.length; i++) { if (!sub.elements[i].isObject3D) { throw new Error(` Invalid object for attached layer to update. Must be a THREE.Object and have a THREE.Material`); } // update attached layers for (const attachedLayer of geometryLayer.attachedLayers) { if (attachedLayer.ready) { attachedLayer.update(context, attachedLayer, sub.elements[i], sub.parent); } } } } } updateElements(context, geometryLayer, newElementsToUpdate); } } function filterChangeSources(updateSources, geometryLayer) { let fullUpdate = false; const filtered = new Set(); updateSources.forEach(src => { if (src === geometryLayer || src.isCamera) { geometryLayer.info.clear(); fullUpdate = true; } else if (src.layer === geometryLayer) { filtered.add(src); } }); return fullUpdate ? new Set([geometryLayer]) : filtered; } class MainLoop extends EventDispatcher { #needsRedraw = false; #updateLoopRestarted = true; #lastTimestamp = 0; constructor(scheduler, engine) { super(); this.renderingState = RENDERING_PAUSED; this.scheduler = scheduler; this.gfxEngine = engine; // TODO: remove me } scheduleViewUpdate(view, forceRedraw) { this.#needsRedraw |= forceRedraw; if (this.renderingState !== RENDERING_SCHEDULED) { this.renderingState = RENDERING_SCHEDULED; // TODO Fix asynchronization between xr and MainLoop render loops. // WebGLRenderer#setAnimationLoop must be used for WebXR projects. // (see WebXR#initializeWebXR). if (!this.gfxEngine.renderer.xr.isPresenting) { requestAnimationFrame(timestamp => { this.step(view, timestamp); }); } } } #update(view, updateSources, dt) { const context = { camera: view.camera, engine: this.gfxEngine, scheduler: this.scheduler, view }; // replace layer with their parent where needed updateSources.forEach(src => { const layer = src.layer || src; if (layer.isLayer && layer.parent) { updateSources.add(layer.parent); } }); for (const geometryLayer of view.getLayers((x, y) => !y)) { context.geometryLayer = geometryLayer; if (geometryLayer.ready && geometryLayer.visible && !geometryLayer.frozen) { view.execFrameRequesters(MAIN_LOOP_EVENTS.BEFORE_LAYER_UPDATE, dt, this.#updateLoopRestarted, geometryLayer); // Filter updateSources that are relevant for the geometryLayer const srcs = filterChangeSources(updateSources, geometryLayer); if (srcs.size > 0) { // pre update attached layer for (const attachedLayer of geometryLayer.attachedLayers) { if (attachedLayer.ready && attachedLayer.preUpdate) { attachedLayer.preUpdate(context, srcs); } } // `preUpdate` returns an array of elements to update const elementsToUpdate = geometryLayer.preUpdate(context, srcs); // `update` is called in `updateElements`. updateElements(context, geometryLayer, elementsToUpdate); // `postUpdate` is called when this geom layer update process is finished geometryLayer.postUpdate(context, geometryLayer, updateSources); } // Clear the cache of expired resources view.execFrameRequesters(MAIN_LOOP_EVENTS.AFTER_LAYER_UPDATE, dt, this.#updateLoopRestarted, geometryLayer); } } } step(view, timestamp) { const dt = timestamp - this.#lastTimestamp; view._executeFrameRequestersRemovals(); view.execFrameRequesters(MAIN_LOOP_EVENTS.UPDATE_START, dt, this.#updateLoopRestarted); const willRedraw = this.#needsRedraw; this.#lastTimestamp = timestamp; // Reset internal state before calling _update (so future calls to View.notifyChange() // can properly change it) this.#needsRedraw = false; this.renderingState = RENDERING_PAUSED; const updateSources = new Set(view._changeSources); view._changeSources.clear(); // update camera const dim = this.gfxEngine.getWindowSize(); view.execFrameRequesters(MAIN_LOOP_EVENTS.BEFORE_CAMERA_UPDATE, dt, this.#updateLoopRestarted); view.camera.update(dim.x, dim.y); view.execFrameRequesters(MAIN_LOOP_EVENTS.AFTER_CAMERA_UPDATE, dt, this.#updateLoopRestarted); // Disable camera's matrix auto update to make sure the camera's // world matrix is never updated mid-update. // Otherwise inconsistencies can appear because object visibility // testing and object drawing could be performed using different // camera matrixWorld. // Note: this is required at least because WEBGLRenderer calls // camera.updateMatrixWorld() const oldAutoUpdate = view.camera3D.matrixAutoUpdate; view.camera3D.matrixAutoUpdate = false; // update data-structure this.#update(view, updateSources, dt); if (this.scheduler.commandsWaitingExecutionCount() == 0) { this.dispatchEvent({ type: 'command-queue-empty' }); } // Redraw *only* if needed. // (redraws only happen when this.#needsRedraw is true, which in turn only happens when // view.notifyChange() is called with redraw=true) // As such there's no continuous update-loop, instead we use a ad-hoc update/render // mechanism. if (willRedraw) { this.#renderView(view, dt); } // next time, we'll consider that we've just started the loop if we are still PAUSED now this.#updateLoopRestarted = this.renderingState === RENDERING_PAUSED; view.camera3D.matrixAutoUpdate = oldAutoUpdate; view.execFrameRequesters(MAIN_LOOP_EVENTS.UPDATE_END, dt, this.#updateLoopRestarted); } #renderView(view, dt) { view.execFrameRequesters(MAIN_LOOP_EVENTS.BEFORE_RENDER, dt, this.#updateLoopRestarted); if (view.render) { view.render(); } else { // use default rendering method this.gfxEngine.renderView(view); } view.execFrameRequesters(MAIN_LOOP_EVENTS.AFTER_RENDER, dt, this.#updateLoopRestarted); } } export default MainLoop;