获取浏览器刷新率的一致方法

Consistent way to get a browser's refresh rate

我使用增量时间来测量循环中各帧之间经过的时间。 time 变量的值用作动画速度的乘数。这可确保拥有 60 Hz 显示器的用户将体验到与拥有 144 Hz 显示器的用户相同的动画速度(尽管帧速率不同)。如果没有这个倍增器,144 赫兹的用户体验到的动画效果会比使用 60 赫兹显示器的人快得多。我就是这样实现的:

let time;
let last = window.performance.now();

function loop(current) {
    time = current - last;
    last = current;
    
    // run game logic and render
    
    requestAnimationFrame(loop);
}

尽管这种测量效果很好,但它也有缺陷。浏览器不像本机应用程序那样一致,并且 time 变量的值在卡顿期间会有很大差异,从而导致“跳跃”动画。

有没有更好的方法来检测浏览器的帧速率而不依赖于 requestAnimationFrame 的不一致数字?

帧速率 运行 平均值。

由于浏览器上的计时器不是很准确,因此最好使用 运行 平均值。

一个运行均值是前n帧的平均值。

从运行均值我们可以通过选择高于均值

的closest帧率来估计帧率

例子

该示例通过对象 FrameRate

实现了 运行 的意思
  • 创建 const rate = FrameRate(sample) 其中样本 os 要取平均值的样本数。

  • 要添加示例,请将任何值分配给 属性 rate.tick 它使用 performance.now 来测量时间。

  • rate.rate 以 fps 为单位保存当前平均值。如果没有足够的样本,它将 return 为 1

  • rate.FPS 保存每秒计算的帧数。这是一个估计值,将在您获得该值时设置丢帧数。

  • rate.dropped 是在当前帧率下每个样本计数丢弃的估计帧数 returned by rate.FPS 注 dropped 应该在 [= 之后查询24=]

function FrameRate(samples = 20) {
    const times = [];
    var s = samples;
    while(s--) { times.push(0) }
    var head = 0, total = 0, frame = 0, previouseNow = 0, rate = 0, dropped = 0;
    const rates = [0, 10, 12, 15, 20, 30, 60, 90, 120, 144, 240];
    const rateSet = rates.length;
    const API = {
        sampleCount: samples,
        reset() {
            frame = total = head = 0;
            previouseNow = performance.now();
            times.fill(0);
        },
        set tick(soak) {
            const now = performance.now()
            total -= times[head];
            total += (times[head++] = now - previouseNow);
            head %= samples;
            frame ++;
            previouseNow = now
        },
        get rate() { return frame > samples ? 1000 / (total / samples) : 1 },
        get FPS() {
            var r = API.rate, rr = r | 0, i = 0;
            while (i < rateSet && rr > rates[i]) { i++ }
            rate = rates[i];
            dropped = Math.round((total - samples * (1000 / rate)) / (1000 / rate));
            return rate;
        },
        get dropped() { return dropped },
    };
    return API;
}

const fRate = FrameRate();
var frame = 0;
requestAnimationFrame(loop);
fRate.reset();
function loop() {
    frame++;
    fRate.tick = 1;
    meanRateEl.textContent = "Mean FPS: " + fRate.rate.toFixed(3);
    FPSEl.textContent = "FPS: " + fRate.FPS;
    droppedEl.textContent = "Dropped frames: " + fRate.dropped + " per " + fRate.sampleCount + " samples" ;
    requestAnimationFrame(loop);
}
body {user-select: none;}
<div id="meanRateEl"></div>
<div id="FPSEl"></div>
<div id="droppedEl"></div>

如果您获得一致的 FPS 90、120、144 或 240,并且您没有禁用帧限制 and/or Vsnyc 并且没有使用 FireFox,请发表评论。目前还不清楚有多少人将这些设置设为默认

示例 2

  • 该示例让您设置 3 个帧速率。它通过调用多个帧请求来实现。对于 240FPS,它要求每次同步 4 帧

  • 如果您在浏览器上关闭帧垂直同步,帧速率 120 和 240 将无法准确报告。要查看实际帧速率,只需使用 60FPS 设置。

  • 您可以给框架添加负载。负载是所请求帧速率的帧时间的一小部分。

var forceRate = 60;
var current = 60;
var load = 0, loadTime = 0;

function FrameRate(samples = 20) {
    const times = [];
    var s = samples;
    while(s--) { times.push(0) }
    var head = 0, total = 0, frame = 0, previouseNow = 0, rate = 0, dropped = 0;
    const rates = [0, 10, 12, 15, 20, 30, 60, 90, 120, 144, 240];
    const rateSet = rates.length;
    const API = {
        sampleCount: samples,
        reset() {
            frame = total = head = 0;
            previouseNow = performance.now();
            times.fill(0);
        },
        set tick(soak) {
            const now = performance.now()
            total -= times[head];
            total += (times[head++] = now - previouseNow);
            head %= samples;
            frame ++;
            previouseNow = now
        },
        get rate() { return frame > samples ? 1000 / (total / samples) : 1 },
        get FPS() {
            var r = API.rate, rr = r | 0, i = 0;
            while (i < rateSet && rr > rates[i]) { i++ }
            rate = rates[i];
            dropped = Math.round((total - samples * (1000 / rate)) / (1000 / rate));
            return rate;
        },
        get dropped() { return dropped },
    };
    return API;
}

const fRate = FrameRate();
var frame = 0;
requestAnimationFrame(loop);
fRate.reset();
function loop() {
    frame++;
    fRate.tick = 1;
    if (load) {
        const pnow = performance.now() + loadTime;
        while (performance.now() < pnow);        
    }
    meanRateEl.textContent = "Mean FPS: " + fRate.rate.toFixed(3);
    FPSEl.textContent = "FPS: " + fRate.FPS;
    droppedEl.textContent = "Dropped frames: " + fRate.dropped + " per " + fRate.sampleCount + " samples" ;
    
    if (current > forceRate) {
        current /= 2;
    } else {
        requestAnimationFrame(loop);
        if (current < forceRate) {
            current *= 2;
            requestAnimationFrame(loop);
        }
    }

}
options.addEventListener("click", (e) => {
    if (e.target.dataset.load !== undefined) {
        load = Number(e.target.dataset.load);
        loadTime = (1000 / forceRate) * load;
        loadEl.textContent = "Load " + loadTime.toFixed(3) + "ms " + (load * 100).toFixed(0) + "%";
        return;
    }
    if (e.target.dataset.rate !== undefined && e.target.dataset.rate != forceRate) {
        forceRate = e.target.dataset.rate | 0;
        forcedRateEl.textContent = "Requested " + forceRate + "FPS";
        loadTime = (1000 / forceRate) * load;
        loadEl.textContent = "Load " + loadTime.toFixed(3) + "ms " + (load * 100).toFixed(0) + "%";        
   }
});
body {user-select: none;}
<div id="forcedRateEl">Requested 60FPS</div>
<div id="loadEl">Load 0ms 0% frame time</div>
<div id="meanRateEl"></div>
<div id="FPSEl"></div>
<div id="droppedEl"></div>
<div id="options">
Frame rate</br>
<button data-rate="240">240FPS</button>
<button data-rate="120">120FPS</button>
<button data-rate="60">60FPS</button><br>
Frame load</br>
<button data-load="0.0">0%</button>
<button data-load="0.25">25%</button>
<button data-load="0.5">50%</button>
<button data-load="0.75">75%</button>
<button data-load="0.90">90%</button>
<button data-load="1.1">110%</button>
<button data-load="1.5">150%</button>
<button data-load="2">200%</button>
</div>

用户体验

如果您的游戏经常承受重负载导致帧率下降(比如从 60 降到 30),用户会注意到,最好将帧率上限设置为 30,以便游戏保持稳定一致的速率和减速没有被注意到。

另请注意,将帧速率加倍 更多 而不是渲染场景所需的 CPU 时间减半。如果允许用户控制帧速率会导致游戏频繁地在不同速率之间跳跃,这可能会导致对游戏质量的负面看法,这在帧速率较高时更有可能发生。

98% 的用户无法分辨 60 和 30FPS 之间的区别,除非他们进行并排比较。甚至 os只要你有一个一致的帧率,并且它在人类视觉持久性之上,只要在游戏中添加一个虚假的帧率指示器,所有用户都可以被完全愚弄。