UNPKG

@polygonjs/polygonjs

Version:

node-based WebGL 3D engine https://polygonjs.com

343 lines (320 loc) 10.7 kB
import {PolyScene} from '../PolyScene'; import {CoreGraphNode} from '../../../core/graph/CoreGraphNode'; import {SceneEvent} from '../../poly/SceneEvent'; import {Clock} from 'three'; import {SCENE_EVENT_PLAY_EVENT_CONTEXT, SCENE_EVENT_PAUSE_EVENT_CONTEXT} from './events/SceneEventsController'; import {ref} from '../../../core/reactivity/CoreReactivity'; import {Poly} from '../../Poly'; // ensure that FPS remains a float // to have divisions and multiplications also give a float const FPS = 60.0; export const DESIRED_FPS = 60.0; const MAX_DELTA = 0.1; export type onTimeTickHook = (delta: number) => void; export type onPlayingStateChangeCallback = () => void; // const performance = Poly.performance.performanceManager(); export interface TimeControllerUpdateTimeOptions { updateClockDelta?: boolean; } export const TIME_CONTROLLER_UPDATE_TIME_OPTIONS_DEFAULT: TimeControllerUpdateTimeOptions = { updateClockDelta: false, }; type onTimeTickCallbacksMap = Map<string, onTimeTickHook>; export class TimeController { static START_FRAME: Readonly<number> = 0; private _frame: number = 0; private _timeUniform = ref<number>(0); private _timeDeltaUniform = ref<number>(0); private _graphNode: CoreGraphNode; private _realtimeState = true; private _maxFrame = 600; private _maxFrameLocked = false; private _playing: boolean = false; private _clock = new Clock(); // private _delta: number = 0; // private _PLAY_EVENT_CONTEXT: EventContext<SceneEvent> | undefined; // private _PAUSE_EVENT_CONTEXT: EventContext<SceneEvent> | undefined; // private _TICK_EVENT_CONTEXT: EventContext<SceneEvent> | undefined; // get PLAY_EVENT_CONTEXT() { // return (this._PLAY_EVENT_CONTEXT = this._PLAY_EVENT_CONTEXT || {event: new SceneEvent(SceneEventType.PLAY)}); // } // get PAUSE_EVENT_CONTEXT() { // return (this._PAUSE_EVENT_CONTEXT = this._PAUSE_EVENT_CONTEXT || {event: new SceneEvent(SceneEventType.PAUSE)}); // } // get TICK_EVENT_CONTEXT() { // return (this._TICK_EVENT_CONTEXT = this._TICK_EVENT_CONTEXT || {event: new SceneEvent(SceneEventType.TICK)}); // } constructor(private scene: PolyScene) { this._graphNode = new CoreGraphNode(scene, 'timeController'); } get graphNode() { return this._graphNode; } dispose() { this._graphNode.dispose(); } updateClockDelta() { const delta = this._clock.getDelta(); const clampedDelta = delta > MAX_DELTA ? MAX_DELTA : delta; return this.setDelta(clampedDelta); } delta() { return this._timeDeltaUniform.value; } setDelta(delta: number) { return (this._timeDeltaUniform.value = delta); } frame(): number { return this._frame; } timeUniform() { return this._timeUniform; } timeDeltaUniform() { return this._timeDeltaUniform; } time(): number { return this._timeUniform.value; } // timeDelta() { // return this._timeDeltaUniform.value; // } maxFrame() { return this._maxFrame; } maxFrameLocked() { return this._maxFrameLocked; } realtimeState() { return this._realtimeState; } setMaxFrame(maxFrame: number) { this._maxFrame = Math.floor(maxFrame); this.scene.dispatchController.dispatch(this._graphNode, SceneEvent.MAX_FRAME_UPDATED); } setMaxFrameLocked(state: boolean) { this._maxFrameLocked = state; this.scene.dispatchController.dispatch(this._graphNode, SceneEvent.MAX_FRAME_UPDATED); } setRealtimeState(state: boolean) { this._realtimeState = state; this.scene.dispatchController.dispatch(this._graphNode, SceneEvent.REALTIME_STATUS_UPDATED); } setTime(time: number, updateFrame = true) { if (time == this._timeUniform.value) { return; } this._timeUniform.value = time; // we block updates here, so that dependent nodes only cook once this.scene.cooker.block(); const delta = this.delta(); for (const callback of this._onBeforeTickCallbacks) { callback(delta); } if (this._playing == true && time > 0) { this.scene.actorsManager.tick(); } if (updateFrame) { const newFrame = Math.floor(this._timeUniform.value * FPS); const boundedFrame = this._ensureFrameWithinBounds(newFrame); if (newFrame != boundedFrame) { this.setFrame(boundedFrame, true); } else { this._frame = newFrame; } } // update time dependents this.scene.dispatchController.dispatch(this._graphNode, SceneEvent.FRAME_UPDATED); // this.scene.uniformsController.updateTime(); this.graphNode.setSuccessorsDirty(); this.scene.cooker.unblock(); // dispatch events after nodes have cooked // this.scene.eventsDispatcher.sceneEventsController.dispatch(SCENE_EVENT_TICK_EVENT_CONTEXT); for (const callback of this._onAfterTickCallbacks) { callback(delta); } } setFrame(frame: number, updateTime = true) { if (frame == this._frame) { return; } frame = this._ensureFrameWithinBounds(frame); if (frame == this._frame) { return; } this._frame = frame; if (this._frame == TimeController.START_FRAME) { this.scene.actorsManager.runOnEventSceneReset(); } if (updateTime) { this.setTime(this._frame / FPS, false); } } setFrameToStart() { this.setFrame(TimeController.START_FRAME, true); } incrementTimeIfPlaying(options?: TimeControllerUpdateTimeOptions) { if (this._playing) { if (!this.scene.root().areChildrenCooking()) { this.incrementTime(options); } } } incrementTime(options?: TimeControllerUpdateTimeOptions) { if (!(options?.updateClockDelta == false)) { this.updateClockDelta(); } if (this._realtimeState) { // const performance_now = performance.now(); const newTime = this._timeUniform.value + this.delta(); // this._prev_performance_now = performance_now; this.setTime(newTime, false); this.setFrame(this._frame + 1, false); } else { this.setFrame(this.frame() + 1); } } private _ensureFrameWithinBounds(frame: number): number { if (this._playing) { if (this._maxFrameLocked && frame > this._maxFrame) { return TimeController.START_FRAME; } } else { if (this._maxFrameLocked && frame > this._maxFrame) { return this._maxFrame; } if (frame < TimeController.START_FRAME) { return TimeController.START_FRAME; } } return frame; } playing() { return this._playing === true; } pause() { if (this._playing == false) { return; } this._playing = false; // TODO: try and unify the dispatch controller and events dispatcher this.scene.dispatchController.dispatch(this._graphNode, SceneEvent.PLAY_STATE_UPDATED); this.scene.actorsManager.runOnEventScenePause(); this.scene.eventsDispatcher.sceneEventsController.dispatch(SCENE_EVENT_PAUSE_EVENT_CONTEXT); for (const callback of this._onPlayingStateChangeCallbacks) { callback(); } } play() { if (!this._playAllowedWithoutAllNodesCooked) { if (!this.scene.cookController.allNodesHaveCookedAtLeastOnce()) { Poly.warn('play not allowed, some nodes have not completed cooking'); return; } } if (this._playing == true) { return; } this._playing = true; this.scene.actorsManager.runOnEventScenePlay(); this.scene.dispatchController.dispatch(this._graphNode, SceneEvent.PLAY_STATE_UPDATED); this.scene.eventsDispatcher.sceneEventsController.dispatch(SCENE_EVENT_PLAY_EVENT_CONTEXT); for (const callback of this._onPlayingStateChangeCallbacks) { callback(); } } togglePlayPause() { if (this.playing()) { this.pause(); } else { this.play(); } } private _playAllowedWithoutAllNodesCooked = true; forbidPlayUntilAllNodesCooked() { this._playAllowedWithoutAllNodesCooked = false; } // // // CALLBACKS // // private _onBeforeTickCallbacksMap: onTimeTickCallbacksMap | undefined; private _onAfterTickCallbacksMap: onTimeTickCallbacksMap | undefined; private _onPlayingStateChangeCallbacksMap: Set<onPlayingStateChangeCallback> | undefined; private _onBeforeTickCallbacks: Array<onTimeTickHook> = []; private _onAfterTickCallbacks: Array<onTimeTickHook> = []; private _onPlayingStateChangeCallbacks: Array<onPlayingStateChangeCallback> = []; registerOnBeforeTick(callbackName: string, callback: onTimeTickHook) { this._registerCallback(callbackName, callback, this.registeredBeforeTickCallbacks()); } unRegisterOnBeforeTick(callbackName: string) { this._unregisterCallback(callbackName, this._onBeforeTickCallbacksMap); } registeredBeforeTickCallbacks() { return (this._onBeforeTickCallbacksMap = this._onBeforeTickCallbacksMap || new Map()); } hasBeforeTickCallback(callbackName: string): boolean { return this._onBeforeTickCallbacksMap?.has(callbackName) || false; } registerOnAfterTick(callbackName: string, callback: onTimeTickHook) { this._registerCallback(callbackName, callback, this.registeredAfterTickCallbacks()); } unRegisterOnAfterTick(callbackName: string) { this._unregisterCallback(callbackName, this._onAfterTickCallbacksMap); } registeredAfterTickCallbacks() { return (this._onAfterTickCallbacksMap = this._onAfterTickCallbacksMap || (new Map() as onTimeTickCallbacksMap)); } hasAfterTickCallback(callbackName: string): boolean { return this._onAfterTickCallbacksMap?.has(callbackName) || false; } onPlayingStateChange(callback: onPlayingStateChangeCallback) { this._onPlayingStateChangeCallbacksMap = this._onPlayingStateChangeCallbacksMap || new Set(); this._onPlayingStateChangeCallbacksMap.add(callback); this._updateOnPlayingStateChangeCallbacks(); } removeOnPlayingStateChange(callback: onPlayingStateChangeCallback) { if (this._onPlayingStateChangeCallbacksMap) { this._onPlayingStateChangeCallbacksMap.delete(callback); this._updateOnPlayingStateChangeCallbacks(); } } private _updateOnPlayingStateChangeCallbacks() { this._onPlayingStateChangeCallbacks = []; if (this._onPlayingStateChangeCallbacksMap) { this._onPlayingStateChangeCallbacksMap.forEach((callback) => { this._onPlayingStateChangeCallbacks.push(callback); }); } } private _registerCallback<C extends onTimeTickHook>( callbackName: string, callback: C, map: onTimeTickCallbacksMap ) { if (map.has(callbackName)) { console.warn(`callback ${callbackName} already registered`); return; } map.set(callbackName, callback); this._updateCallbacks(); } private _unregisterCallback(callbackName: string, map?: onTimeTickCallbacksMap) { if (!map) { return; } map.delete(callbackName); this._updateCallbacks(); } private _updateCallbacks() { this._onBeforeTickCallbacks.length = 0; this._onBeforeTickCallbacksMap?.forEach((callback) => { this._onBeforeTickCallbacks.push(callback); }); this._onAfterTickCallbacks.length = 0; this._onAfterTickCallbacksMap?.forEach((callback) => { this._onAfterTickCallbacks.push(callback); }); } }