three.js icon indicating copy to clipboard operation
three.js copied to clipboard

Added THREE.Timer.

Open Mugen87 opened this issue 5 years ago • 13 comments

Related #17679

This is a new alternative to THREE.Clock with a different API design and behavior. It should make it easier to implement three.js apps FPS independent which becomes more and more important with faster refresh rates.

Background: Some time ago, I've realized on a 144 Hz gaming laptop that (at least in Chrome and Firefox) WebGL apps run with 144 FPS. Some three.js examples now look weird because they assume fix update steps (around 60 FPS). Since certain laptops already offer 240 Hz screens, I think we should reconsider how a time class works. The main differences of THREE.Timer compared to THREE.Clock are:

  • THREE.Timer has an update method that updates its internal state.
  • That makes it possible to call .getDeltaTime() and .getElapsedTime() multiple times per simulation step without getting different values.
  • The class uses the Page Visibility API to avoid large time delta values when the app is inactive (e.g. tab switched or browser hidden).
  • It's possible to configure a fixed time delta and a time scale value (similar to Unity's Time interface).
  • No public properties, no ctor parameter. The public API only consists of methods.

THREE.Timer is located in examples/jsm/misc/ for now. I personally think THREE.Timer (or a similar class) should replace THREE.Clock at some point since the class has some conceptual design flaws. The example webgl_morphtargets_sphere already uses THREE.Timer in order to replace the existing FPS workaround.

The method names are quite descriptive now. If e.g. getDelta() is preferred over getDeltaTime(), I'm happy to shorten the names.

Mugen87 avatar Nov 12 '19 14:11 Mugen87

I like it! Just a question, what is the expected use of now() if fixedTime is enabled? maybe is safer to return elapsedTime in that case? as it could be inconsistent to use now() together with the elapsedTime provided by the fixed steps.

fernandojsg avatar Nov 13 '19 10:11 fernandojsg

Just a question, what is the expected use of now() if fixedTime is enabled?

Um, it's probably better to make now() private in order to avoid this confusion. The method should just return a time stamp which is used to compute the delta time when no fixed deltas are used.

Timer.now() should not be affected by timeScale. If elapsedTime is used in now(), that would be the case.

Mugen87 avatar Nov 13 '19 10:11 Mugen87

So in that case I agree on making now() private

fernandojsg avatar Nov 13 '19 10:11 fernandojsg

If getDelta() is preferred over getDeltaTime(), I'm happy to shorten the names.

I think getDelta() is fine - in the context of a timer, it's very clear that we are talking about time deltas.

looeee avatar Nov 13 '19 12:11 looeee

In what situations would you want to use fixedDeltaTime?

looeee avatar Nov 13 '19 13:11 looeee

It's actually useful for testing and debugging if a timer can produce deterministic values.

Mugen87 avatar Nov 13 '19 13:11 Mugen87

I think getDelta() is fine - in the context of a timer, it's very clear that we are talking about time deltas.

Okay, I've shorten the method names and variables.

Mugen87 avatar Nov 13 '19 21:11 Mugen87

Nice.

Is there a reason you've chosen to add a separate "update()" call?

I.e. why not update the timer when "getDelta()" is called like the current Clock?

CreaticDD avatar Jan 09 '20 21:01 CreaticDD

I.e. why not update the timer when "getDelta()" is called like the current Clock?

Because many users complained about the fact that multiple calls of getDelta() within a single frame produces wrong results. getElapsedTime() has similar issues, see #5696.

To avoid these design problems, the state of the timer is now explicitly updated by calling update(). Yes, it is an additional method call but it avoids confusing behaviors of the current Clock class.

Mugen87 avatar Jan 10 '20 10:01 Mugen87

Should also the Timer class have the serialization/deserialization methods? They would be nice to have. So that it would be possible to "save" and then "restore" the state of the timer (except ._previousTime and ._currentTime).

LeviPesin avatar Mar 17 '22 08:03 LeviPesin

Sserialization/deserialization is only added to core classes which are part of the scene graph. Clock and Timer are not added to the scene.

Mugen87 avatar Mar 17 '22 09:03 Mugen87

@Mugen87 When this PR would be merged?

LeviPesin avatar Apr 08 '22 08:04 LeviPesin

@LeviPesin TBH, I don't think it's a good idea to post at PRs asking for when are they getting merged or if merge conflicts can be resolved. PRs are paused/stalled for various reasons. Sometimes their general approach isn't right, sometimes collaborators are not happy with certain parts or have a different solutions in mind. However, not every collaborator has the time or opportunity to explain their concerns in detail because other tasks are more important.

Looking at the past, many stalled PR were merged when the team was confident enough with the respective design decision.

Mugen87 avatar Apr 08 '22 08:04 Mugen87

we started to add it to three-stdlib, i noticed that the class has constructor side effects, for reference: https://github.com/mrdoob/three.js/issues/20575

i've added a connect method that pairs with dispose https://github.com/pmndrs/three-stdlib/blob/main/src/misc/Timer.ts that would also make the object pausable/re-usable.

drcmda avatar Dec 31 '22 08:12 drcmda

I've also been using a modified Timer class in @pmndrs/postprocessing which could serve as feedback for the API design.

This version uses getter properties instead of methods and has an autoReset setting that controls usage of the page visibility API. The update method also has an optional timestamp parameter which can be obtained from the requestAnimationFrame callback to avoid unnecessary calls to performance.now:

requestAnimationFrame(function render(timestamp: number): void {

	requestAnimationFrame(render);
	timer.update(timestamp);

});

vanruesc avatar Dec 31 '22 14:12 vanruesc

The update method also has an optional timestamp parameter which can be obtained from the requestAnimationFrame callback to avoid unnecessary calls to performance.now:

That is a good idea! I'll add it to the PR.

Mugen87 avatar Dec 31 '22 15:12 Mugen87

I think there may be a bug in _now() function.

let timer = new Timer();

// Let it run to for example 10 seconds.

timer = null;

// Wait 10 seconds.

timer = new Timer();

timer.getElapsed(); // returns 0 (as expected)
timer.update();
timer.getElapsed(); // returns 20, so there is a sudden gap of 20 seconds (unexpected)

Shouldn't it count from 0 after the second initialization?

manake avatar Mar 30 '23 17:03 manake

For reproduction: https://jsfiddle.net/3kejsft2/

Indeed! I guess I have designed Timer without the assumption somebody wants to creation multiple instances of it.

Can you please verify if this version of Timer fixes your issue? https://jsfiddle.net/uje7xw2r/

The idea is to save the start time value of the timer and then just subtract it from the timestamp.

Mugen87 avatar Mar 30 '23 17:03 Mugen87

Thank you!

It seems to work now.

(There's also a minor inconsistency that if ( document.hidden === false ) this.reset(); is lacking curly braces but all other if statements have them (one-liners and multiple-liners).)

manake avatar Mar 30 '23 19:03 manake

would you mind elaborate that how the Timer solves this "design flaw" of Clock https://github.com/mrdoob/three.js/pull/26806#issuecomment-1727706775? any help is appreciated.

ycw avatar Sep 21 '23 12:09 ycw

Updating the timer per frame before querying data is the intended usage.

Mugen87 avatar Sep 21 '23 14:09 Mugen87

I've seen projects in the past copying this code or use an adapted version of it so I think it's better to provide an alternative to THREE.Clock in three.js.

Mugen87 avatar Dec 16 '23 14:12 Mugen87

The update method also has an optional timestamp parameter which can be obtained from the requestAnimationFrame callback to avoid unnecessary calls to performance.now:

That is a good idea! I'll add it to the PR.

Just curious: why was this removed in d443665? :thinking:

vanruesc avatar Dec 16 '23 19:12 vanruesc

I personally think THREE.Timer (or a similar class) should replace THREE.Clock at some point

How about adding it to core now, with documentation? Then, deprecate THREE.Clock eventually.

If you don't want to do that now, how about adding the above comprehensive description to the header of Timer.js?

WestLangley avatar Dec 16 '23 19:12 WestLangley

Just curious: why was this removed in https://github.com/mrdoob/three.js/commit/d443665a59479f728f7de41ac58a9dd4254a4c91? 🤔

I was unsure about this feature since the parameter has no effect when a fixed time delta is used. Besides, I couldn't decide how the parameter should play together with _startTime (which was added to support multiple timers).

That said, not including a timestamp parameter in this PR does not mean we shouldn't support it. I just had the feeling this needs more discussion. A separate PR adding timestamp would be fine!

If you don't want to do that now, how about adding the above comprehensive description to the header of Timer.js?

That sounds good to me!

Mugen87 avatar Dec 16 '23 20:12 Mugen87

If you don't want to do that now, how about adding the above comprehensive description to the header of Timer.js?

That sounds good to me!

Done! 46b6bb0ab34427b36bc25c4747db2bcdb88905bf

For r161, I'll try to add a documentation page.

Mugen87 avatar Dec 17 '23 09:12 Mugen87

Sorry for the huge delay dealing with this... 🙏

mrdoob avatar Dec 21 '23 11:12 mrdoob

Hmmm... I'm not sure about the API for fixedTime. I'll open a new PR for it... 🤔

mrdoob avatar Dec 21 '23 11:12 mrdoob