Limiting framerate in Three.js to increase performance, requestAnimationFrame?
Asked Answered
R

7

43

I was thinking that for some projects I do 60fps is not totally needed. I figured I could have more objects and things that ran at 30fps if I could get it to run smoothly at that framerate. I figured if I edited the requestAnimationFrame shim inside of three.js I could limit it to 30 that way. But I was wondering if there was a better way to do this using three.js itself as provided. Also, will this give me the kind of performance increase I am thinking. Will I be able to render twice as many objects at 30fps as I will at 60? I know the difference between running things at 30 and 60, but will I be able to get it to run at a smooth constant 30fps?

I generally use the WebGLRenderer, and fall back to Canvas if needed except for projects that are targeting one specifically, and typically those are webgl shader projects.

Reactant answered 1/7, 2012 at 19:39 Comment(0)
H
83

What about something like this:

function animate() {

    setTimeout( function() {

        requestAnimationFrame( animate );

    }, 1000 / 30 );

    renderer.render();

}
Hadley answered 2/7, 2012 at 14:15 Comment(5)
Any reason to consider this instead of other answers which refer to external clocks?Iodine
Interesting. Well I'll bite. There are so many reasons why this is the best answer... basically if you read about rAF it explains why you should use it in the last mile controlling when to render your scene. rAF allows you to be informed when your monitor hardware refresh is ready for a frame. This is important because if you can deliver your frame in a reasonably timely fashion, you can produce a perfectly smooth animation. There is no hope of doing such a thing if you take timing into your own hands with timers that have no input on monitor refresh timing. It works like a metronome.Lamoreaux
Timing things and modernizing for a good visual experience with all these high frequency capable display technologies maturing is actually a pretty deep rabbit hole. based on what we can see in terms of progress here issues.chromium.org/issues/41367944 I hope in the near future requestAnimationFrame may need to be superceded or extended with a new paradigm that allows for VRR capability so that if the web application desires, it may be able to dictate the display sync timepoints, rather than the other way around. This would provide more efficient utilization of GPU resources.Lamoreaux
Either way you do it: EITHER your application informs to the monitor it's done rendering the frame, so it can perform the display of that frame immediately, OR the application is to receive the signal so it can make an attempt to stay in phase with the display interval by launching the render function in sync with it, these are the ways to make flawless animations. The former situation is what VRR enables, and is ideal, since with a fixed interval if you need more time than 16ms to render your frame, you miss the flight and hop on the next one, leading to dropping to a 30fps update frequency.Lamoreaux
With VRR, you can render in 20ms, and the monitor compensates by presenting frames at 50fps, so you can get a non-juddery animation while your GPU continues working full bore.Lamoreaux
M
28

This approach could also work, using the THREE.Clock to measure the delta.

let clock = new THREE.Clock();
let delta = 0;
// 30 fps
let interval = 1 / 30;

function update() {
  requestAnimationFrame(update);
  delta += clock.getDelta();

   if (delta  > interval) {
       // The draw or time dependent code are here
       render();

       delta = delta % interval;
   }
}
Malisamalison answered 21/8, 2018 at 6:42 Comment(5)
This works better than the accepted answer, at least for me. My sprite animations would pause for a few millis after each loop when using the setTimeout approach. This suggestion has no such negative effect, while still reducing CPU load equally well.Lacuna
I think this is how you are supposed to do it, smart and clean. I also don't feel very comfortable memory-wise with calling setTimeout multiple times in a second,. It could be totally fine but still.Hanes
Interestingly enough mrdoob suggested setTimeout even though THREE.Clock already existed back in 2012. In any case, I'm also skeptical of the timeout and this answer is solid!Bayless
This solution also works great for limiting heavy calculations (ex: game physics). If you prefer uncapped FPS, you can calculate the physics inside the interval, call the render function outside the if-condition, then interpolate the 3D objects using the interval delta. Example: twitter.com/varunprime/status/1591274302686445568Napiform
In general this seems like a great idea to me. However, you can simplify the code by using clock.start(), which resets the initial time whenever it's called, along with clock.getElapsedTime(). const clock = new THREE.Clock(); const interval = 1 / 30; function update() { requestAnimationFrame( animate ); if ( clock.getElapsedTime() < interval ) return; clock.start(); render(); }Som
M
6

The amount of work your CPU and GPU needs to do depend on the workload and they set the upper limit of smooth framerate.

  • GPU works mostly linearly and can always push out the same count of polygons to screen.

  • However, if you have doubled the number of objects CPU must work harder to animate these all objects (matrix transformationsn and such). It depends on your world model and other work Javascript does how much extra overhead is given. Also the conditions like the number of visible objects is important.

For simple models where all polygons are on the screen always then it should pretty much follow the rule "half the framerate, double the objects". For 3D shooter like scenes this is definitely not the case.

Medor answered 2/7, 2012 at 8:52 Comment(0)
X
6

I came across this article which gives two ways of solving the custom frame rate issue.

http://codetheory.in/controlling-the-frame-rate-with-requestanimationframe/

I think this way is more robust, as it will have a steady animation speed even on computers that do not render the canvas consistently at 60 fps. Below is an example

var now,delta,then = Date.now();
var interval = 1000/30;

function animate() {
    requestAnimationFrame (animate);
    now = Date.now();
    delta = now - then;
    //update time dependent animations here at 30 fps
    if (delta > interval) {
        sphereMesh.quaternion.multiplyQuaternions(autoRotationQuaternion, sphereMesh.quaternion);
        then = now - (delta % interval);
    }
    render();
}
Xanthippe answered 11/12, 2017 at 21:6 Comment(0)
H
4

The accepted answer has a problem and gives up to -10fps on slow computers compared to not limiting the fps, so for example without limiting 36pfs, with the accepted solution 26fps (for a 60fps, 1000/60 setTimeout).

Instead you can do this:

var dt=1000/60;
var timeTarget=0;
function render(){
  if(Date.now()>=timeTarget){

    gameLogic();
    renderer.render();

    timeTarget+=dt;
    if(Date.now()>=timeTarget){
      timeTarget=Date.now();
    }
  }
  requestAnimationFrame(render);
}

That way is not going to wait if its already behind.

Heathenize answered 25/8, 2019 at 2:55 Comment(0)
M
3

The previous answers seem to ignore the intended design of requestAnimationFrame, and make some extraneous calls as a result. requestAnimationFrame takes a callback, which in turn takes a high precision timestamp as its argument. So you know the current time and you don't need to call Date.now(), or any other variant, since you already have the time. All that’s needed is basic arithmetic:

var frameLengthMS = 1000/60;//60 fps
var previousTime = 0;

function render(timestamp){
  if(timestamp - previousTime > frameLengthMS){
   /* your rendering logic goes here */
       drawSomething();
   /* * * * */
    previousTime = timestamp;
  }
  requestAnimationFrame(render);
}
Marin answered 24/11, 2022 at 17:48 Comment(0)
K
0

In my case, using setTimeout reduces FPS out of the box. Even with high fps like 50, it reduces to 30 because of the logic. I found a working solution for me on this fiddle: https://jsfiddle.net/m1erickson/CtsY3/

I changed Date.now() to performance.now() //We do not need full date

 gameTimeUpdate() {
    this.deltaTime = (performance.now() - this.time)/1000;
    requestAnimationFrame(this.gameTimeUpdate);

    const now = performance.now();
    const elapsed = now - this.lastUpdateTime;
    if (elapsed < fpsInterval) return;
    //skip if too big FPS
    stats.update(); // -> ThreeJs stats will update with changes FPS
    this.lastUpdateTime = now - (elapsed % fpsInterval);    //fpsInterval = 30
    this.time = performance.now();
    
Kelp answered 28/3 at 9:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.