@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
343 lines (320 loc) • 10.7 kB
text/typescript
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);
});
}
}