用 requestAnimationFrame 控制 fps?

Posted

技术标签:

【中文标题】用 requestAnimationFrame 控制 fps?【英文标题】:Controlling fps with requestAnimationFrame? 【发布时间】:2013-11-14 20:28:20 【问题描述】:

现在看来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();
    

Node.drawFlash() 只是一些根据计数器变量确定半径然后绘制圆的代码。

【问题讨论】:

您的动画是否滞后?我认为requestAnimationFrame 的最大优势是(顾名思义)仅在需要时才请求动画帧。假设你展示了一个静态的黑色画布,你应该得到 0 fps,因为不需要新的帧。但是,如果您正在显示需要 60fps 的动画,您也应该得到它。 rAF 只允许“跳过”无用的帧,然后节省 CPU。 setInterval 在非活动标签中也不起作用。 此代码在 90hz 显示器、60hz 显示器和 144hz 显示器上的运行方式不同。 【参考方案1】:

如何将 requestAnimationFrame 限制为特定帧速率

5 FPS 的演示节流:http://jsfiddle.net/m1erickson/CtsY3/

此方法通过测试自执行最后一帧循环以来经过的时间来工作。

您的绘图代码仅在您指定的 FPS 间隔已过时执行。

代码的第一部分设置了一些用于计算经过时间的变量。

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();

这段代码是实际的 requestAnimationFrame 循环,它以您指定的 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

    

【讨论】:

不错的演示 - 它应该被接受。在这里,用你的小提琴来演示使用 window.performance.now() 而不是 Date.now()。这与 rAF 已经收到的高分辨率时间戳非常吻合,因此无需在回调中调用 Date.now():jsfiddle.net/chicagogrooves/nRpVD/2 感谢您使用新的 rAF 时间戳功能更新链接。新的 rAF 时间戳增加了有用的基础设施,而且比 Date.now 更精确。 这是一个非常好的演示,它启发了我制作自己的演示 (JSFiddle)。主要区别在于使用 rAF(如 Dean 的演示)而不是 Date、添加控件以动态调整目标帧率、在动画的单独间隔上采样帧率以及添加历史帧率图表。 有人能解释一下 elapsed % fpsInterval 部分吗?为什么我们需要“同时调整您指定的 fpsInterval 不是 RAF 间隔 (16.7ms) 的倍数”? 您可以控制的只是何时跳帧。一个 60 fps 的监视器总是以 16 毫秒的间隔绘制。例如,如果您希望游戏以 50fps 运行,您希望每 6 帧跳过一次。您检查 20 毫秒 (1000/50) 是否已经过去,但没有(仅 16 毫秒)因此您跳过一帧,然后下一帧自您绘制以来已过去 32 毫秒,因此您绘制并重置。但随后您将跳过一半帧并以 30fps 运行。因此,当您重置时,您会记得上次等待 12 毫秒的时间太长了。所以下一帧又过了 16 毫秒,但你把它算作 16+12=28 毫秒,所以你再次绘制,你等了 8 毫秒太长了【参考方案2】:

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)
    

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

// 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;
    
;

用法

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

var fc = new FpsCtrl(24, function(e) 
     // render each frame here
  );

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

fc.start();

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

演示

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;
		
	;
body font:16px sans-serif
<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>

旧答案

requestAnimationFrame 的主要目的是将更新同步到显示器的刷新率。这将要求您以显示器的 FPS 或其一个因素(即 60、30、15 FPS 的典型刷新率 @ 60 Hz)进行动画处理。

如果您想要更随意的 FPS,那么使用 rAF 毫无意义,因为帧速率无论如何都不会匹配显示器的更新频率(只是这里和那里的一帧),这根本无法为您提供流畅的动画(与所有帧一样)重新计时),您也可以使用setTimeoutsetInterval

当您想以不同的 FPS 播放视频,然后设备刷新时,这也是专业视频行业的一个众所周知的问题。已经使用了许多技术,例如帧混合和复杂的重新定时基于运动矢量重新构建中间帧,但是对于画布,这些技术不可用,结果总是会出现抖动的视频。

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

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

    ... code for frame here

我们将setTimeout放在首位的原因(以及为什么在使用poly-fill时将rAF放在首位)是因为setTimeout会排队,这样会更准确循环开始时立即发生一个事件,这样无论剩余代码将使用多少时间(只要它不超过超时间隔),下一次调用都将在它表示的间隔内(对于纯 rAF,这不是 rAF 必不可少的无论如何都会尝试跳到下一帧)。

另外值得注意的是,将它放在首位也可能会像setInterval 一样冒着调用堆叠的风险。 setInterval 可能更准确一些。

您可以使用setInterval 代替在循环之外执行相同操作。

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

function loop() 

    ... code for frame here

并停止循环:

clearInterval(rememberMe);

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

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

这样您可以将 FPS 降低到 1/4 等。

【讨论】:

在某些情况下,您不会尝试匹配显示器的帧速率,而是要在图像序列中匹配丢帧。顺便说一句,很好的解释 限制 requestAnimationFrame 的最大原因之一是将某些代码的执行与浏览器的动画帧对齐。事情最终会变得更加顺畅,特别是如果您在每一帧的数据上运行一些逻辑,例如使用音乐可视化工具。 这很糟糕,因为 requestAnimationFrame 的主要用途是同步 DOM 操作(读/写),所以不使用它会在访问 DOM 时影响性能,因为操作不会排队等待执行一起,并会强制布局重新绘制不必要的。 没有“调用堆叠”的风险,因为 javascript 运行单线程,并且在您的代码运行时不会触发超时事件。因此,如果函数花费的时间比超时时间长,它几乎会尽可能快地运行,而浏览器仍然会在调用之间进行重绘并触发其他超时。 我知道您说页面刷新的更新速度不能超过显示器的 fps 限制。但是,是否可以通过触发页面重排来更快地刷新?相反,如果多页重排的完成速度比原生 fps 速率快,是否可能不会注意到它们?【参考方案3】:

我找到了一个很好的解释:CreativeJS.com,在传递给 requestAnimationFrame 的函数内包装 setTimeou) 调用。我对“普通” requestionAnimationFrame 的担忧是,“如果我只想要它每秒动画三次怎么办?”即使使用 requestAnimationFrame(相对于 setTimeout),它仍然浪费(一些)“能量”(意味着浏览器代码正在做某事,并可能减慢系统速度)60 或 120 或每秒多少次,而不是每秒只有 2 次或 3 次(如您所愿)。

大多数时候,出于这个原因,我故意关闭 使用 JavaScript 运行我的浏览器。但是,我使用的是 Yosemite 10.10.3,我认为它存在某种计时器问题 - 至少在我的旧系统上(相对较旧 - 意思是 2011 年)。

【讨论】:

setTimeout 会导致严重的卡顿,不应在生产游戏中使用。【参考方案4】:

跳过 requestAnimationFrame 会导致在自定义 fps 下不流畅(所需的)动画。

// Input/output DOM elements
var $results = $("#results");
var $fps = $("#fps");
var $period = $("#period");

// Array of FPS samples for graphing

// Animation state/parameters
var fpsInterval, lastDrawTime, frameCount_timed, frameCount, lastSampleTime, 
		currentFps=0, currentFps_timed=0;
var intervalID, requestID;

// Setup canvas being animated
var canvas = document.getElementById("c");
var canvas_timed = document.getElementById("c2");
canvas_timed.width = canvas.width = 300;
canvas_timed.height = canvas.height = 300;
var ctx = canvas.getContext("2d");
var ctx2 = canvas_timed.getContext("2d");


// Setup input event handlers

$fps.on('click change keyup', function() 
    if (this.value > 0) 
        fpsInterval = 1000 / +this.value;
    
);

$period.on('click change keyup', function() 
    if (this.value > 0) 
        if (intervalID) 
            clearInterval(intervalID);
        
        intervalID = setInterval(sampleFps, +this.value);
    
);


function startAnimating(fps, sampleFreq) 

    ctx.fillStyle = ctx2.fillStyle = "#000";
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx2.fillRect(0, 0, canvas.width, canvas.height);
    ctx2.font = ctx.font = "32px sans";
    
    fpsInterval = 1000 / fps;
    lastDrawTime = performance.now();
    lastSampleTime = lastDrawTime;
    frameCount = 0;
    frameCount_timed = 0;
    animate();
    
    intervalID = setInterval(sampleFps, sampleFreq);
		animate_timed()


function sampleFps() 
    // sample FPS
    var now = performance.now();
    if (frameCount > 0) 
        currentFps =
            (frameCount / (now - lastSampleTime) * 1000).toFixed(2);
        currentFps_timed =
            (frameCount_timed / (now - lastSampleTime) * 1000).toFixed(2);
        $results.text(currentFps + " | " + currentFps_timed);
        
        frameCount = 0;
        frameCount_timed = 0;
    
    lastSampleTime = now;


function drawNextFrame(now, canvas, ctx, fpsCount) 
    // Just draw an oscillating seconds-hand
    
    var length = Math.min(canvas.width, canvas.height) / 2.1;
    var step = 15000;
    var theta = (now % step) / step * 2 * Math.PI;

    var xCenter = canvas.width / 2;
    var yCenter = canvas.height / 2;
    
    var x = xCenter + length * Math.cos(theta);
    var y = yCenter + length * Math.sin(theta);
    
    ctx.beginPath();
    ctx.moveTo(xCenter, yCenter);
    ctx.lineTo(x, y);
  	ctx.fillStyle = ctx.strokeStyle = 'white';
    ctx.stroke();
    
    var theta2 = theta + 3.14/6;
    
    ctx.beginPath();
    ctx.moveTo(xCenter, yCenter);
    ctx.lineTo(x, y);
    ctx.arc(xCenter, yCenter, length*2, theta, theta2);

    ctx.fillStyle = "rgba(0,0,0,.1)"
    ctx.fill();
    
    ctx.fillStyle = "#000";
    ctx.fillRect(0,0,100,30);
    
    ctx.fillStyle = "#080";
    ctx.fillText(fpsCount,10,30);


// redraw second canvas each fpsInterval (1000/fps)
function animate_timed() 
    frameCount_timed++;
    drawNextFrame( performance.now(), canvas_timed, ctx2, currentFps_timed);
    
    setTimeout(animate_timed, fpsInterval);


function animate(now) 
    // request another frame
    requestAnimationFrame(animate);
    
    // calc elapsed time since last loop
    var elapsed = now - lastDrawTime;

    // if enough time has elapsed, draw the next frame
    if (elapsed > fpsInterval) 
        // Get ready for next frame by setting lastDrawTime=now, but...
        // Also, adjust for fpsInterval not being multiple of 16.67
        lastDrawTime = now - (elapsed % fpsInterval);

        frameCount++;
    		drawNextFrame(now, canvas, ctx, currentFps);
    

startAnimating(+$fps.val(), +$period.val());
input
  width:100px;

#tvs
  color:red;
  padding:0px 25px;

H3
  font-weight:400;
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<h3>requestAnimationFrame skipping <span id="tvs">vs.</span> setTimeout() redraw</h3>
<div>
    <input id="fps" type="number" value="33"/> FPS:
    <span id="results"></span>
</div>
<div>
    <input id="period" type="number" value="1000"/> Sample period (fps, ms)
</div>
<canvas id="c"></canvas><canvas id="c2"></canvas>

@tavnab 的原始代码。

【讨论】:

【参考方案5】:

我建议将您对requestAnimationFrame 的呼叫封装在setTimeout 中:

const fps = 25;
function animate() 
  // perform some animation task here

  setTimeout(() => 
    requestAnimationFrame(animate);
  , 1000 / fps);

animate();

您需要在setTimeout 中调用requestAnimationFrame,而不是反过来,因为requestAnimationFrame 会安排您的函数在下一次重绘之前运行,如果您进一步延迟更新,请使用setTimeout将错过那个时间窗口。但是,反向操作是合理的,因为您只是在发出请求之前等待一段时间。

【讨论】:

这实际上似乎可以降低帧速率,因此不会占用我的 CPU。它是如此简单。干杯! 对于轻量级动画来说,这是一个不错的简单方法。不过,至少在某些设备上,它确实有点不同步。我在我以前的一个引擎上使用了这种技术。在事情变得复杂之前效果很好。最大的问题是当连接到方向传感器时,它要么落后要么变得神经质。后来我发现使用单独的 setInterval 并通过对象属性在传感器、setInterval 帧和 RAF 帧之间进行通信更新允许传感器和 RAF 实时运行,而动画时间可以通过 setInterval 的属性更新来控制。 最佳答案!谢谢;) 我的显示器是 60 FPS,如果我设置 var fps=60,使用这个代码我只能得到大约 50 FPS。我想把它放慢到 60,因为有些人有 120 FPS 的显示器,但我不想影响其他人。这非常困难。 FPS 低于预期的原因是 setTimeout 可以在超过指定延迟后执行回调。这有很多可能的原因。并且每个循环都需要时间来设置一个新的计时器并在设置新的超时之前执行一些代码。您无法准确地做到这一点,您应该始终考虑比预期慢的结果,但只要您不知道它会慢多少,尝试降低延迟也是不准确的。浏览器中的 JS 并不意味着那么准确。【参考方案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);

来源:A Detailed Explanation of JavaScript Game Loops and Timing by Isaac Sukin

【讨论】:

如果我的显示器以 60 FPS 运行,而我希望我的游戏以 58 FPS 运行,我设置 maxFPS=58,这将使它以 30 FPS 运行,因为它会跳过每 2 帧。 是的,我也试过这个。我选择不实际限制 RAF 本身——只有更改由 setTimeout 更新。根据 DevTools 中的读数,至少在 Chrome 中,这会导致有效 fps 以 setTimeouts 速度运行。当然,它只能以显卡和显示器刷新率的速度更新真实视频帧,但这种方法似乎运行起来最慢,所以最流畅的“明显”fps 控制,这就是我想要的。 因为我跟踪 JS 对象中的所有运动与 RAF 分开,所以无论 RAF 或 setTimeout ,加上一点额外的数学知识。【参考方案7】:

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

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

我知道有两种方法可以在没有卡顿的情况下非常有效,使用更少的果汁并产生更少的热量。准确的人工计时和机器效率是最终结果。

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


方法一:通过setInterval更新数据,通过RAF更新图形。

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

在 requestAnimationFrame 循环中,使用老式的 for 循环遍历您的数组——不要在这里使用较新的形式,它们很慢!

for(var i=0; i<sprite.length-1; i++)  rafUpdate(sprite[i]);  

在您的 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

这会使您对数据对象和变换字符串的更新同步到 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]

方法 2. 将 SI 放入 web-worker。这一款非常流畅!

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

一次只有一个人可以读写。这很好,只要他们检查它是否未定义以避免错误。 RAF 速度很快,会立即将其踢回,然后通过一堆 GPU 帧检查它是否已被发回。 web-worker 中的 SI 将大部分时间拥有 sprite 数组,并将更新位置、运动和物理数据,以及创建新的转换字符串,然后将其传递回页面中的 RAF。

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

它会很顺利地做到这一点,没有卡顿,但在实际指定的帧速率下,几乎没有分歧。


结果:

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

【讨论】:

附带说明 - 在方法 1 中,如果您的 setInterval 中有太多活动,则可能会由于单线程异步而减慢您的 RAF。您可以在 SI 框架上减轻这种对活动的破坏,因此异步将更快地将控制权传递回 RAF。请记住,RAF 以最大帧速率运行,但会与显示同步图形更改,因此可以跳过几个 RAF 帧 - 只要您跳过的帧不超过 SI 帧,它就不会卡顿。 方法 2 更健壮,因为它实际上是对两个循环进行多任务处理,而不是通过异步来回切换,但您仍然希望避免 SI 帧花费比您想要的帧速率更长的时间,因此如果需要进行大量数据操作而需要多个 SI 帧才能完成,那么拆分 SI 活动可能仍然是可取的。 我认为值得一提的是,像这样运行成对循环实际上在 Chromes DevTools 中注册了 GPU 以 setInterval 循环中指定的帧速率运行!似乎只有发生图形变化的 RAF 帧才被 FPS 计计为帧。因此,就 GPU 而言,仅非图形工作或什至只是空白循环的 RAF 帧不计算在内。我觉得这很有趣,可以作为进一步研究的起点。 P.S.我做了一些阅读,似乎大多数浏览器在后台选项卡中将定时事件限制为每秒一次(这可能也应该以某种方式处理)。如果您仍想解决问题并在不可见时完全暂停,似乎有visibilitychange 事件。 你没有。您在网络工作者中进行计算并将结果发送给消息。除此之外,您仍在使用相同的方式运行您的 RAF。您可以类似地通过 iframe 运行另一个线程。消息传递的工作原理基本相同。我还没有尝试过 iframe 的想法。无论哪种方式,它都会将计算放在一个单独的线程中,而不是运行 RAF 和间隔帧的部分。【参考方案8】:

我总是以非常简单的方式做到这一点,而不会弄乱时间戳:

let fps, eachNthFrame, frameCount;

fps = 30;

//This variable specifies how many frames should be skipped.
//If it is 1 then no frames are skipped. If it is 2, one frame 
//is skipped so "eachSecondFrame" is renderd.
eachNthFrame = Math.round((1000 / fps) / 16.66);

//This variable is the number of the current frame. It is set to eachNthFrame so that the 
//first frame will be renderd.
frameCount = eachNthFrame;

requestAnimationFrame(frame);

//I think the rest is self-explanatory
function frame() 
  if (frameCount === eachNthFrame) 
    frameCount = 0;
    animate();
  
  frameCount++;
  requestAnimationFrame(frame);

【讨论】:

如果您的显示器是 120 fps,这将运行得太快。【参考方案9】:
var time = 0;
var time_framerate = 1000; //in milliseconds

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

    //your code
  

  window.requestAnimationFrame(animate);

【讨论】:

请添加几句话来解释您的代码在做什么,以便您的回答获得更多的支持。【参考方案10】:

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

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 ...

重要的是要知道 requestAnimationFrame 取决于用户监视器的刷新率 (vsync)。因此,如果您没有在模拟中使用单独的计时器机制,例如依靠 requestAnimationFrame 来获得游戏速度,这将使其无法在 200Hz 显示器上播放。

【讨论】:

这是在three.js v106上唯一对我有用的解决方案【参考方案11】:

如需将 FPS 限制为任意值,请参阅 jdmayfields answer。 但是,对于将帧速率减半的非常快速简便的解决方案,您可以通过以下方式仅每隔 2 帧进行一次计算:

requestAnimationFrame(render);
function render() 
  // ... computations ...
  requestAnimationFrame(skipFrame);

function skipFrame()  requestAnimationFrame(render); 

同样,您可以随时调用render,但使用一个变量来控制这次是否进行计算,从而允许您将 FPS 减少到三分之一或四分之一(在我的情况下,对于示意图 webgl-animation 20fps 是仍然足够,同时大大降低了客户端的计算负载)

【讨论】:

【参考方案12】:

最简单的方法

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();

【讨论】:

稳定吗?【参考方案13】:

我尝试了针对此问题提供的多种解决方案。尽管解决方案按预期工作,但它们导致的输出并不那么专业。

根据我的个人经验,我强烈建议不要在浏览器端控制 FPS,尤其是使用 requestAnimationFrame。因为,当你这样做时,它会使帧渲染体验非常不稳定,用户会清楚地看到帧跳跃,最后看起来一点也不真实或专业。

因此,我的建议是在发送自身时从服务器端控制 FPS,并在浏览器端收到帧后立即渲染它们。

注意:如果您仍想在客户端进行控制,请尽量避免 在控制 fps 的逻辑中使用 setTimeout 或 Date 对象。 因为,当 FPS 很高时,这些会在 事件循环或对象创建方面的术语。

【讨论】:

以上是关于用 requestAnimationFrame 控制 fps?的主要内容,如果未能解决你的问题,请参考以下文章

用 requestAnimationFrame 控制 fps?

requestAnimationFrame动画方法

如何停止requestAnimationFrame方法启动的动画

在Android设备上不支持requestAnimationFrame

canvas动态画圆弧及requestAnimationFrame

canvas动态画圆弧及requestAnimationFrame