使用requestAnimationFrame控制fps?

rob*_*uan 111 javascript performance animation canvas requestanimationframe

这似乎requestAnimationFrame是现在制作动画的事实上的方式.在大多数情况下,它对我来说效果很好,但是现在我正在尝试做一些画布动画,我想知道:有没有办法确保它以某个fps运行?我知道rAF的目的是为了一贯平滑的动画,我可能冒着使我的动画不稳定的风险,但是现在看起来它的速度几乎是任意的,并且我想知道是否有办法打击不知何故.

我使用setInterval但我想要rAF提供的优化(特别是当选项卡处于焦点时自动停止).

如果有人想查看我的代码,它几乎是:

animateFlash: function() {
    ctx_fg.clearRect(0,0,canvasWidth,canvasHeight);
    ctx_fg.fillStyle = 'rgba(177,39,116,1)';
    ctx_fg.strokeStyle = 'none';
    ctx_fg.beginPath();
    for(var i in nodes) {
        nodes[i].drawFlash();
    }
    ctx_fg.fill();
    ctx_fg.closePath();
    var instance = this;
    var rafID = requestAnimationFrame(function(){
        instance.animateFlash();
    })

    var unfinishedNodes = nodes.filter(function(elem){
        return elem.timer < timerMax;
    });

    if(unfinishedNodes.length === 0) {
        console.log("done");
        cancelAnimationFrame(rafID);
        instance.animate();
    }
}
Run Code Online (Sandbox Code Playgroud)

其中Node.drawFlash()只是一些代码,它根据计数器变量确定半径,然后绘制一个圆.

mar*_*rkE 154

How to throttle requestAnimationFrame to a specific frame rate

Demo throttling at 5 FPS: http://jsfiddle.net/m1erickson/CtsY3/

This method works by testing the elapsed time since executing the last frame loop.

Your drawing code executes only when your specified FPS interval has elapsed.

The first part of the code sets some variables used to calculate elapsed time.

var stop = false;
var frameCount = 0;
var $results = $("#results");
var fps, fpsInterval, startTime, now, then, elapsed;


// initialize the timer variables and start the animation

function startAnimating(fps) {
    fpsInterval = 1000 / fps;
    then = Date.now();
    startTime = then;
    animate();
}
Run Code Online (Sandbox Code Playgroud)

And this code is the actual requestAnimationFrame loop which draws at your specified FPS.

// the animation loop calculates time elapsed since the last loop
// and only draws if your specified fps interval is achieved

function animate() {

    // request another frame

    requestAnimationFrame(animate);

    // calc elapsed time since last loop

    now = Date.now();
    elapsed = now - then;

    // if enough time has elapsed, draw the next frame

    if (elapsed > fpsInterval) {

        // Get ready for next frame by setting then=now, but also adjust for your
        // specified fpsInterval not being a multiple of RAF's interval (16.7ms)
        then = now - (elapsed % fpsInterval);

        // Put your drawing code here

    }
}
Run Code Online (Sandbox Code Playgroud)

  • 很好的演示 - 它应该被接受.在这里,分叉你的小提琴,演示使用window.performance.now()而不是Date.now().这与rAF已经收到的高分辨率时间戳非常吻合,所以不需要在回调中调用Date.now():http://jsfiddle.net/chicagogrooves/nRpVD/2/ (11认同)
  • 这是一个非常好的演示,它激发了我自己的演绎([JSFiddle](http://jsfiddle.net/21q6Lgyx/5/)).主要区别在于使用rAF(如Dean的演示)而不是Date,添加控件以动态调整目标帧速率,在与动画分开的间隔内采样帧率,以及添加历史帧率图表. (10认同)
  • 优秀的解释和例子.这应该标记为已接受的答案 (4认同)
  • 感谢使用新的rAF时间戳功能的更新链接.新的rAF时间戳增加了有用的基础设施,它也比Date.now更精确. (2认同)
  • 您所能控制的只是何时要跳过一帧。60 fps 监视器始终以 16 毫秒的间隔进行绘制。例如,如果您希望游戏以 50fps 运行,您希望每 6 帧跳过一次。您检查是否 20 毫秒 (1000/50) 已经过去,并且还没有(仅过去了 16 毫秒),因此您跳过一帧,然后下一帧自您绘制以来已经过去了 32 毫秒,因此您绘制并重置。但是你会跳过一半的帧并以 30fps 的速度运行。所以当你重置时,你记得上次你等了 12 毫秒太久了。所以下一帧又过了 16 毫秒,但你把它算作 16+12=28 毫秒,所以你再次绘制,你等了 8 毫秒太久了 (2认同)

小智 38

2016/6更新

限制帧速率的问题是屏幕具有恒定的更新速率,通常为60 FPS.

如果我们想要24 FPS,我们永远不会在屏幕上获得真正的24 fps,我们可以这样计时但不显示它,因为显示器只能显示15 fps,30 fps或60 fps的同步帧(有些显示器也是120 fps ).

但是,为了计时,我们可以在可能的情况下进行计算和更新

您可以通过将计算和回调封装到对象中来构建控制帧速率的所有逻辑:

function FpsCtrl(fps, callback) {

    var delay = 1000 / fps,                               // calc. time per frame
        time = null,                                      // start time
        frame = -1,                                       // frame count
        tref;                                             // rAF time reference

    function loop(timestamp) {
        if (time === null) time = timestamp;              // init start time
        var seg = Math.floor((timestamp - time) / delay); // calc frame no.
        if (seg > frame) {                                // moved to next frame?
            frame = seg;                                  // update
            callback({                                    // callback function
                time: timestamp,
                frame: frame
            })
        }
        tref = requestAnimationFrame(loop)
    }
}
Run Code Online (Sandbox Code Playgroud)

然后添加一些控制器和配置代码:

// play status
this.isPlaying = false;

// set frame-rate
this.frameRate = function(newfps) {
    if (!arguments.length) return fps;
    fps = newfps;
    delay = 1000 / fps;
    frame = -1;
    time = null;
};

// enable starting/pausing of the object
this.start = function() {
    if (!this.isPlaying) {
        this.isPlaying = true;
        tref = requestAnimationFrame(loop);
    }
};

this.pause = function() {
    if (this.isPlaying) {
        cancelAnimationFrame(tref);
        this.isPlaying = false;
        time = null;
        frame = -1;
    }
};
Run Code Online (Sandbox Code Playgroud)

用法

它变得非常简单 - 现在,我们所要做的就是通过设置回调函数和所需的帧速率来创建一个实例,如下所示:

var fc = new FpsCtrl(24, function(e) {
     // render each frame here
  });
Run Code Online (Sandbox Code Playgroud)

然后开始(如果需要,可以是默认行为):

fc.start();
Run Code Online (Sandbox Code Playgroud)

就是这样,所有逻辑都在内部处理.

演示

var ctx = c.getContext("2d"), pTime = 0, mTime = 0, x = 0;
ctx.font = "20px sans-serif";

// update canvas with some information and animation
var fps = new FpsCtrl(12, function(e) {
	ctx.clearRect(0, 0, c.width, c.height);
	ctx.fillText("FPS: " + fps.frameRate() + 
                 " Frame: " + e.frame + 
                 " Time: " + (e.time - pTime).toFixed(1), 4, 30);
	pTime = e.time;
	var x = (pTime - mTime) * 0.1;
	if (x > c.width) mTime = pTime;
	ctx.fillRect(x, 50, 10, 10)
})

// start the loop
fps.start();

// UI
bState.onclick = function() {
	fps.isPlaying ? fps.pause() : fps.start();
};

sFPS.onchange = function() {
	fps.frameRate(+this.value)
};

function FpsCtrl(fps, callback) {

	var	delay = 1000 / fps,
		time = null,
		frame = -1,
		tref;

	function loop(timestamp) {
		if (time === null) time = timestamp;
		var seg = Math.floor((timestamp - time) / delay);
		if (seg > frame) {
			frame = seg;
			callback({
				time: timestamp,
				frame: frame
			})
		}
		tref = requestAnimationFrame(loop)
	}

	this.isPlaying = false;
	
	this.frameRate = function(newfps) {
		if (!arguments.length) return fps;
		fps = newfps;
		delay = 1000 / fps;
		frame = -1;
		time = null;
	};
	
	this.start = function() {
		if (!this.isPlaying) {
			this.isPlaying = true;
			tref = requestAnimationFrame(loop);
		}
	};
	
	this.pause = function() {
		if (this.isPlaying) {
			cancelAnimationFrame(tref);
			this.isPlaying = false;
			time = null;
			frame = -1;
		}
	};
}
Run Code Online (Sandbox Code Playgroud)
body {font:16px sans-serif}
Run Code Online (Sandbox Code Playgroud)
<label>Framerate: <select id=sFPS>
	<option>12</option>
	<option>15</option>
	<option>24</option>
	<option>25</option>
	<option>29.97</option>
	<option>30</option>
	<option>60</option>
</select></label><br>
<canvas id=c height=60></canvas><br>
<button id=bState>Start/Stop</button>
Run Code Online (Sandbox Code Playgroud)

老答案

主要目的requestAnimationFrame是将更新同步到监视器的刷新率.这将要求您在显示器的FPS或其系数的动画处设置动画(即,对于60 Hz的典型刷新率,为60,30,15 FPS).

如果你想要一个更随意的FPS,那么使用rAF是没有意义的,因为帧速率永远不会与显示器的更新频率相匹配(只是这里和那里的一个帧),它根本无法给你一个平滑的动画(就像所有的帧重新定时一样) )你也可以使用setTimeoutsetInterval代替.

当您想要以不同的FPS播放视频然后显示其刷新的设备时,这也是专业视频行业中众所周知的问题.已经使用了许多技术,例如帧混合和基于运动矢量的复杂重新定时重建中间帧,但是使用画布这些技术不可用并且结果将始终是生涩的视频.

var FPS = 24;  /// "silver screen"
var isPlaying = true;

function loop() {
    if (isPlaying) setTimeout(loop, 1000 / FPS);

    ... code for frame here
}
Run Code Online (Sandbox Code Playgroud)

我们setTimeout 首先放置的原因(以及为什么某些地方rAF在使用poly-fill时首先放置)的原因是,这将更加准确,因为setTimeout当循环开始时,将立即对事件进行排队,这样无论剩余代码将使用多长时间(假设它没有超过超时间隔)下一次调用将是它所代表的间隔(对于纯rAF,这不是必需的,因为rAF将尝试在任何情况下跳转到下一帧).

另外值得注意的是,将它放在第一位也会冒着堆叠的风险setInterval.setInterval对于这种用途可能稍微准确一些.

你也可以使用setInterval,而不是循环做同样的.

var FPS = 29.97;   /// NTSC
var rememberMe = setInterval(loop, 1000 / FPS);

function loop() {

    ... code for frame here
}
Run Code Online (Sandbox Code Playgroud)

并停止循环:

clearInterval(rememberMe);
Run Code Online (Sandbox Code Playgroud)

为了在标签变得模糊时降低帧速率,您可以添加如下因素:

var isFocus = 1;
var FPS = 25;

function loop() {
    setTimeout(loop, 1000 / (isFocus * FPS)); /// note the change here

    ... code for frame here
}

window.onblur = function() {
    isFocus = 0.5; /// reduce FPS to half   
}

window.onfocus = function() {
    isFocus = 1; /// full FPS
}
Run Code Online (Sandbox Code Playgroud)

这样你可以将FPS降低到1/4等.

  • 在某些情况下,您不是要尝试匹配监视器帧速率,而是在图像序列中,例如丢帧.很棒的解释顺便说一下 (4认同)
  • 这很糟糕,因为`requestAnimationFrame`的主要用途是同步DOM操作(读/写),因此不使用它会损害访问DOM时的性能,因为操作不会排队等待一起执行,并且会不必要地强制进行布局重绘. (4认同)
  • 使用requestAnimationFrame进行限制的最大原因之一是将某些代码的执行与浏览器的动画帧排成一行.事情最终运行得更顺畅,特别是如果你在每一帧都运行一些数据逻辑,比如音乐可视化器. (2认同)

Luk*_*lor 31

我建议把你的电话换成requestAnimationFrame一个setTimeout.如果你setTimeout从你请求动画帧的函数中调用,那么你就失去了目的requestAnimationFrame.但如果你requestAnimationFrame从内部打电话setTimeout顺利工作:

var fps = 25
function animate() {
  setTimeout(function() {
    requestAnimationFrame(animate);
  }, 1000 / fps);
}
Run Code Online (Sandbox Code Playgroud)

  • 这实际上似乎可以降低帧率,因此不会烹饪我的 CPU。它是如此简单。干杯! (2认同)
  • FPS 低于预期的原因是 setTimeout 可以在超过指定延迟后执行回调。这有多种可能的原因。每个循环都需要时间来设置一个新的计时器并在设置新的超时之前执行一些代码。您无法对此进行准确处理,您应该始终考虑比预期慢的结果,但只要您不知道它会慢多少,尝试降低延迟也是不准确的。浏览器中的 JS 并不意味着如此准确。 (2认同)

jdm*_*eld 10

这些都是理论上的好主意,直​​到你深入. 问题是你不能在没有去同步的情况下限制RAF,打败它是现有的目的. 所以你让它以全速运行,并在一个单独的循环中更新你的数据,甚至是一个单独的线程!

是的,我说了.您可以在浏览器中执行多线程JavaScript!

我知道有两种方法可以很好地工作,没有粘性,使用更少的果汁和产生更少的热量.精确的人体计时和机器效率是最终结果.

道歉,如果这有点罗嗦,但这里......


方法1:通过setInterval更新数据,通过RAF更新图形.

使用单独的setInterval更新平移和旋转值,物理,碰撞等.将这些值保存在每个动画元素的对象中.将变换字符串分配给对象中的每个setInterval"frame"中的变量.将这些对象保存在一个数组中.将您的间隔设置为所需的fps,单位为ms:ms =(1000/fps).这保持了一个稳定的时钟,允许任何设备上的相同fps,无论RAF速度如何. 不要在这里将变换分配给元素!

在requestAnimationFrame循环中,使用old-school for循环遍历数组 - 不要在这里使用较新的表单,它们很慢!

for(var i=0; i<sprite.length-1; i++){  rafUpdate(sprite[i]);  }
Run Code Online (Sandbox Code Playgroud)

在rafUpdate函数中,从数组中的js对象获取转换字符串,并获取其元素id.您应该已经将"精灵"元素附加到变量上,或者通过其他方式轻松访问,这样您就不会浪费时间在RAF中"获取"它们.将它们保存在以html id命名的对象中的效果非常好.在它进入SI或RAF之前设置该部分.

使用RAF 更新变换,使用3D变换(即使是2d),并设置css"will-change:transform;" 关于将要改变的元素.这样可以使您的变换尽可能地同步到本机刷新率,在GPU中启动,并告诉浏览器最集中的位置.

所以你应该有类似这样的伪代码...

// refs to elements to be transformed, kept in an array
var element = [
   mario: document.getElementById('mario'),
   luigi: document.getElementById('luigi')
   //...etc.
]

var sprite = [  // read/write this with SI.  read-only from RAF
   mario: { id: mario  ....physics data, id, and updated transform string (from SI) here  },
   luigi: {  id: luigi  .....same  }
   //...and so forth
] // also kept in an array (for efficient iteration)

//update one sprite js object
//data manipulation, CPU tasks for each sprite object
//(physics, collisions, and transform-string updates here.)
//pass the object (by reference).
var SIupdate = function(object){
  // get pos/rot and update with movement
  object.pos.x += object.mov.pos.x;  // example, motion along x axis
  // and so on for y and z movement
  // and xyz rotational motion, scripted scaling etc

  // build transform string ie
  object.transform =
   'translate3d('+
     object.pos.x+','+
     object.pos.y+','+
     object.pos.z+
   ') '+

   // assign rotations, order depends on purpose and set-up. 
   'rotationZ('+object.rot.z+') '+
   'rotationY('+object.rot.y+') '+
   'rotationX('+object.rot.x+') '+

   'scale3d('.... if desired
  ;  //...etc.  include 
}


var fps = 30; //desired controlled frame-rate


// CPU TASKS - SI psuedo-frame data manipulation
setInterval(function(){
  // update each objects data
  for(var i=0; i<sprite.length-1; i++){  SIupdate(sprite[i]);  }
},1000/fps); //  note ms = 1000/fps


// GPU TASKS - RAF callback, real frame graphics updates only
var rAf = function(){
  // update each objects graphics
  for(var i=0; i<sprite.length-1; i++){  rAF.update(sprite[i])  }
  window.requestAnimationFrame(rAF); // loop
}

// assign new transform to sprite's element, only if it's transform has changed.
rAF.update = function(object){     
  if(object.old_transform !== object.transform){
    element[object.id].style.transform = transform;
    object.old_transform = object.transform;
  }
} 

window.requestAnimationFrame(rAF); // begin RAF
Run Code Online (Sandbox Code Playgroud)

这样可以保持对数据对象的更新,并将SI中的字符串同步转换为所需的"帧"速率,并且RAF中的实际变换分配同步到GPU刷新率.因此实际的图形更新仅在RAF中,但是对数据的更改以及构建变换字符串都在SI中,因此没有jankies,但"时间"以所需的帧速率流动.


流:

[setup js sprite objects and html element object references]

[setup RAF and SI single-object update functions]

[start SI at percieved/ideal frame-rate]
  [iterate through js objects, update data transform string for each]
  [loop back to SI]

[start RAF loop]
  [iterate through js objects, read object's transform string and assign it to it's html element]
  [loop back to RAF]
Run Code Online (Sandbox Code Playgroud)

方法2.将SI放入Web工作者.这个是FAAAST并且顺利!

与方法1相同,但将SI放在web-worker中.它将在一个完全独立的线程上运行,然后让页面只处理RAF和UI.将精灵数组来回传递为"可转移对象".这快速了.克隆或序列化没有时间,但它不像是通过引用传递,因为来自另一方的引用被破坏,所以你需要将双方传递到另一方,并且只在存在时更新它们,排序就像在高中时和女朋友来回传递一张便条一样.

一次只能读写一个.这是好的,只要他们检查是否未定义以避免错误.RAF是快速的,会立即将它踢回来,然后通过一堆GPU帧检查它是否已被发回.Web工作者中的SI将在大多数时间内拥有精灵阵列,并将更新位置,移动和物理数据,以及创建新的变换字符串,然后将其传递回页面中的RAF.

这是我所知道的通过脚本动画元素的最快方式.这两个函数将作为两个独立的程序在两个独立的线程上运行,利用多核CPU的方式,而单个js脚本则不会.多线程javascript动画.

并且它会在没有抖动的情况下顺利地完成,但是在实际指定的帧速率下,几乎没有发散.


结果:

这两种方法中的任何一种都可以确保您的脚本在任何PC,手机,平板电脑等上以相同的速度运行(当然,在设备和浏览器的功能范围内).

  • 方法 2 更稳健,因为它实际上是对两个循环进行多任务处理,而不是通过异步来回切换,但您仍然希望避免 SI 帧花费的时间超过所需的帧速率,因此拆分 SI 活动可能仍然是如果需要多个 SI 帧才能完成大量数据操作,则这是可取的。 (2认同)
  • 我相信这个解决方案存在一个问题,即当 rAF 暂停时,它会继续运行,例如因为用户切换到另一个选项卡。 (2认同)
  • 你不知道。您在网络工作者中进行计算并发送结果消息。除此之外,你仍然以同样的方式运行你的英国皇家空军。您可以类似地通过 iframe 运行另一个线程。消息传递的工作原理基本相同。我还没有尝试过 iframe 的想法。无论哪种方式,它都会将计算放在一个单独的线程中,而不是运行 RAF 和间隔帧的部分。 (2认同)

小智 8

最简单的方法

note:在具有不同帧速率的不同屏幕上,它的表现可能有所不同。


const FPS = 30;
let lastTimestamp = 0;


function update(timestamp) {
  requestAnimationFrame(update);
  if (timestamp - lastTimestamp < 1000 / FPS) return;
  
  
   /* <<< PUT YOUR CODE HERE >>>  */

 
  lastTimestamp = timestamp;
}


update();

Run Code Online (Sandbox Code Playgroud)


Rus*_*mov 6

如何轻松节流到特定的 FPS:

// timestamps are ms passed since document creation.
// lastTimestamp can be initialized to 0, if main loop is executed immediately
var lastTimestamp = 0,
    maxFPS = 30,
    timestep = 1000 / maxFPS; // ms for each frame

function main(timestamp) {
    window.requestAnimationFrame(main);

    // skip if timestep ms hasn't passed since last frame
    if (timestamp - lastTimestamp < timestep) return;

    lastTimestamp = timestamp;

    // draw frame here
}

window.requestAnimationFrame(main);
Run Code Online (Sandbox Code Playgroud)

来源:Isaac Sukin 对 JavaScript 游戏循环和计时的详细解释

  • 如果我的显示器以 60 FPS 运行并且我希望我的游戏以 58 FPS 运行,我设置 maxFPS=58,这将使它以 30 FPS 运行,因为它会跳过每第二帧。 (2认同)
  • 是的,我也尝试过这个。我选择不实际限制 RAF 本身——只有 setTimeout 更新更改。根据 DevTools 中的读数,至少在 Chrome 中,这会导致有效 fps 以 setTimeouts 速度运行。当然,它只能以显卡和显示器刷新率的速度更新真实视频帧,但这种方法的运行速度似乎最少,因此最流畅的“明显”fps 控制,这就是我想要的。 (2认同)
  • 由于我独立于 RAF 来跟踪 JS 对象中的所有运动,因此这可以使动画逻辑、碰撞检测或您需要的任何内容以感知上一致的速率运行,无论 RAF 或 setTimeout 如何,只需进行一些额外的数学计算。 (2认同)

mov*_*13h 6

解决此问题的一个简单方法是,如果不需要渲染帧,则从渲染循环返回:

const FPS = 60;
let prevTick = 0;    

function render() 
{
    requestAnimationFrame(render);

    // clamp to fixed framerate
    let now = Math.round(FPS * Date.now() / 1000);
    if (now == prevTick) return;
    prevTick = now;

    // otherwise, do your stuff ...
}
Run Code Online (Sandbox Code Playgroud)

重要的是要知道 requestAnimationFrame 取决于用户监视器刷新率(垂直同步)。因此,如果您在模拟中没有使用单独的计时器机制,那么依靠 requestAnimationFrame 来提高游戏速度将使其无法在 200Hz 显示器上玩。


小智 5

var time = 0;
var time_framerate = 1000; //in milliseconds

function animate(timestamp) {
  if(timestamp > time + time_framerate) {
    time = timestamp;    

    //your code
  }

  window.requestAnimationFrame(animate);
}
Run Code Online (Sandbox Code Playgroud)

  • 请添加几句话来解释您的代码在做什么,这样您的答案可以获得更多的支持。 (2认同)