40

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.

mrdoob
  • 19,334
  • 4
  • 63
  • 62
Cory Gross
  • 36,833
  • 17
  • 68
  • 80

6 Answers6

82

What about something like this:

function animate() {

    setTimeout( function() {

        requestAnimationFrame( animate );

    }, 1000 / 30 );

    renderer.render();

}
mrdoob
  • 19,334
  • 4
  • 63
  • 62
24

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;
   }
}
karlos
  • 349
  • 2
  • 3
  • 1
    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. – Johan Fredrik Varen Nov 22 '21 at 22:12
  • 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. – aelmosalamy Jan 09 '22 at 00:35
  • 1
    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! – Damien Apr 13 '22 at 15:00
  • 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: https://twitter.com/varunprime/status/1591274302686445568 – doppler Nov 16 '22 at 22:18
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.

Mikko Ohtamaa
  • 82,057
  • 50
  • 264
  • 435
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();
}
Elder Snayz
  • 61
  • 1
  • 2
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.

Alexandria
  • 41
  • 3
1

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);
}
baadcafe
  • 63
  • 10