web-dev-qa-db-fra.com

Calculer FPS dans Canvas à l'aide de requestAnimationFrame

Comment pourrais-je calculer le FPS d'une application de jeu canvas? J'ai vu quelques exemples, mais aucun d'eux n'utilise requestAnimationFrame, et je ne sais pas comment y appliquer leurs solutions. Voici mon code:

(function(window, document, undefined){

    var canvas       = document.getElementById("mycanvas"),
        context      = canvas.getContext("2d"),
        width        = canvas.width,
        height       = canvas.height,
        fps          = 0,
        game_running = true,
        show_fps     = true;

    function showFPS(){
        context.fillStyle = "Black";
        context.font      = "normal 16pt Arial";

        context.fillText(fps + " fps", 10, 26);
    }
    function gameLoop(){

        //Clear screen
        context.clearRect(0, 0, width, height);

        if (show_fps) showFPS();

        if (game_running) requestAnimationFrame(gameLoop);

    }
    
    gameLoop();

}(this, this.document))
canvas{
    border: 3px solid #fd3300;
}
<canvas id="mycanvas" width="300" height="150"></canvas>

Soit dit en passant, existe-t-il une bibliothèque que je pourrais ajouter pour surveiller les performances?

31

N'utilisez pas new Date()

Cette API a plusieurs défauts et n'est utile que pour obtenir la date et l'heure actuelles. Pas pour mesurer les intervalles de temps.

L'API Date utilise l'horloge interne du système d'exploitation, qui est constamment mise à jour et synchronisée avec les serveurs de temps NTP. Cela signifie que la vitesse/fréquence de cette horloge est parfois plus rapide et parfois plus lente que la temps réel - et donc pas utilisable pour mesurer les durées et les framerates.

Si quelqu'un modifie l'heure du système (manuellement ou en raison de l'heure d'été), vous pourriez au moins voir le problème si une seule image nécessitait soudainement une heure. Ou un temps négatif. Mais si l'horloge système se synchronise 20% plus rapidement pour se synchroniser avec l'heure mondiale, elle est pratiquement impossible à détecter.

De plus, l'API Date est très imprécise - souvent bien inférieure à 1 ms. Cela le rend particulièrement inutile pour les mesures de fréquence d'images, où une trame de 60 Hz nécessite environ 17 ms.

Utilisez plutôt performance.now()

L'API de performance a été spécialement conçue pour de tels cas d'utilisation et peut être utilisée de manière équivalente à new Date(). Prenez simplement l'une des autres réponses et remplacez new Date() par performance.now(), et vous êtes prêt à partir.

Sources:

Contrairement à Date.now (), les valeurs renvoyées par Performance.now () augmentent toujours à un taux constant, indépendamment de l'horloge système (qui peut être ajustée manuellement ou biaisée par un logiciel comme NTP). Sinon, performance.timing.navigationStart + performance.now () sera approximativement égal à Date.now ().

https://developer.mozilla.org/en-US/docs/Web/API/Performance/now

Et pour les fenêtres:

[Le service de temps] ajuste la fréquence d'horloge locale pour lui permettre de converger vers l'heure correcte. Si la différence de temps entre l'horloge locale et [l'échantillon de temps précis] est trop grande pour être corrigée en ajustant la fréquence d'horloge locale, le service de temps règle l'horloge locale à l'heure correcte.

https://technet.Microsoft.com/en-us/library/cc773013 (v = ws.10) .aspx

24
maja

Vous pouvez garder une trace de la dernière fois que requestAnimFrame a été appelé.

var lastCalledTime;
var fps;

function requestAnimFrame() {

  if(!lastCalledTime) {
     lastCalledTime = Date.now();
     fps = 0;
     return;
  }
  delta = (Date.now() - lastCalledTime)/1000;
  lastCalledTime = Date.now();
  fps = 1/delta;
} 

http://jsfiddle.net/vZP3u/

26
Justin Thomas

Chrome a un compteur fps intégré: https://developer.chrome.com/devtools/docs/rendering-settings =

enter image description here

Ouvrez simplement la console de développement (F12), ouvrez le tiroir (Esc) et ajoutez l'onglet "Rendu".

Ici, vous pouvez activer la superposition FPS-Meter pour voir le taux de rafraîchissement actuel (y compris un joli graphique), ainsi que la consommation de mémoire du GPU.

Solution multi-navigateur: Vous pouvez obtenir une superposition similaire avec la bibliothèque JavaScript stat.js: https://github.com/mrdoob /stats.js/

enter image description here

Il fournit également une superposition agréable pour le framerate (graphique inclus) et est très facile à utiliser.

Lorsque vous comparez les résultats de stats.js et des outils de développement chrome, les deux affichent exactement les mêmes mesures. Vous pouvez donc faire confiance à cette bibliothèque pour faire la bonne chose.

17
maja

J'ai une approche différente, car si vous calculez le FPS, vous obtiendrez ce scintillement lors du retour du nombre. J'ai décidé de compter chaque image et de la retourner une fois par seconde

window.countFPS = (function () {
  var lastLoop = (new Date()).getMilliseconds();
  var count = 1;
  var fps = 0;

  return function () {
    var currentLoop = (new Date()).getMilliseconds();
    if (lastLoop > currentLoop) {
      fps = count;
      count = 1;
    } else {
      count += 1;
    }
    lastLoop = currentLoop;
    return fps;
  };
}());

requestAnimationFrame(function () {
  console.log(countFPS());
});

jsfiddle

8
kernel

Voici une autre solution:

var times = [];
var fps;

function refreshLoop() {
  window.requestAnimationFrame(function() {
    const now = performance.now();
    while (times.length > 0 && times[0] <= now - 1000) {
      times.shift();
    }
    times.Push(now);
    fps = times.length;
    refreshLoop();
  });
}

refreshLoop();

Cela améliore certains des autres des manières suivantes:

  • performance.now() est utilisé sur Date.now() pour une précision accrue ( comme couvert dans cette réponse )
  • Le FPS est mesuré au cours de la dernière seconde afin que le nombre ne saute pas de manière si irrégulière, en particulier pour les applications qui ont des trames longues uniques.

J'ai écrit sur cette solution plus en détail sur mon site Web .

3
Daniel Imms

Juste une preuve de concept. Code très simple. Tout ce que nous faisons est de définir nos images par seconde et les intervalles entre chaque image. Dans la fonction de dessin, nous déduisons le temps d'exécution de notre dernière image de l'heure actuelle pour vérifier si le temps écoulé depuis la dernière image est supérieur à notre intervalle (qui est basé sur les fps) ou non. Si la condition est évaluée à vrai, nous définissons l'heure de notre image actuelle qui sera la "dernière heure d'exécution de l'image" dans le prochain appel de dessin.

var GameLoop = function(fn, fps){
    var now;
    var delta;
    var interval;
    var then = new Date().getTime();

    var frames;
    var oldtime = 0;

    return (function loop(time){
        requestAnimationFrame(loop);

        interval = 1000 / (this.fps || fps || 60);
        now = new Date().getTime();
        delta = now - then;

        if (delta > interval) {
            // update time stuffs
            then = now - (delta % interval);

            // calculate the frames per second
            frames = 1000 / (time - oldtime)
            oldtime = time;

            // call the fn
            // and pass current fps to it
            fn(frames);
        }
    }(0));
};

Usage:

var set;
document.onclick = function(){
    set = true;
};

GameLoop(function(fps){
    if(set) this.fps = 30;
    console.log(fps);
}, 5);

http://jsfiddle.net/ARTsinn/rPAeN/

3
yckart

Vérifiez simplement la différence de temps entre les rappels AFR. AFR passe déjà l'heure en argument au rappel. J'ai mis à jour votre violon pour le montrer: http://jsfiddle.net/WCKhH/1/

3
Gerben

En fait, aucune des réponses ne me suffisait. Voici une meilleure solution qui:

  • Utilisez performance.now ()
  • Calcule la moyenne réelle fps par seconde
  • La moyenne par seconde et les décimales sont configurables

Code:

// Options
const outputEl         = document.getElementById('fps-output');
const decimalPlaces    = 2;
const updateEachSecond = 1;

// Cache values
const decimalPlacesRatio = Math.pow(10, decimalPlaces);
let timeMeasurements     = [];

// Final output
let fps = 0;

const tick = function() {
  timeMeasurements.Push(performance.now());

  const msPassed = timeMeasurements[timeMeasurements.length - 1] - timeMeasurements[0];

  if (msPassed >= updateEachSecond * 1000) {
    fps = Math.round(timeMeasurements.length / msPassed * 1000 * decimalPlacesRatio) / decimalPlacesRatio;
    timeMeasurements = [];
  }

  outputEl.innerText = fps;

  requestAnimationFrame(() => {
    tick();
  });
}

tick();

JSFiddle

3
Mick

Il me manquait une implémentation qui permet de personnaliser la taille de l'échantillon pour la valeur FPS moyenne. Voici le mien, il présente les caractéristiques suivantes:

  • Précis: basé sur performance.now ()
  • Stabilisé: La valeur FPS retournée est une valeur moyenne (fps.value | fps.tick ())
  • Configurable: La taille du tableau d'échantillons FPS peut être personnalisée (fps.samplesSize)
  • efficace: réseau rotatif pour la collecte d'échantillons (évite le redimensionnement du réseau)
const fps = {
    sampleSize : 60,    
    value : 0,
    _sample_ : [],
    _index_ : 0,
    _lastTick_: false,
    tick : function(){
        // if is first tick, just set tick timestamp and return
        if( !this._lastTick_ ){
            this._lastTick_ = performance.now();
            return 0;
        }
        // calculate necessary values to obtain current tick FPS
        let now = performance.now();
        let delta = (now - this._lastTick_)/1000;
        let fps = 1/delta;
        // add to fps samples, current tick fps value 
        this._sample_[ this._index_ ] = Math.round(fps);
        
        // iterate samples to obtain the average
        let average = 0;
        for(i=0; i<this._sample_.length; i++) average += this._sample_[ i ];

        average = Math.round( average / this._sample_.length);

        // set new FPS
        this.value = average;
        // store current timestamp
        this._lastTick_ = now;
        // increase sample index counter, and reset it
        // to 0 if exceded maximum sampleSize limit
        this._index_++;
        if( this._index_ === this.sampleSize) this._index_ = 0;
        return this.value;
    }
}


// *******************
// test time...
// *******************

function loop(){
    let fpsValue = fps.tick();
    window.fps.innerHTML = fpsValue;
    requestAnimationFrame( loop );
}
// set FPS calulation based in the last 120 loop cicles 
fps.sampleSize = 120;
// start loop
loop()
<div id="fps">--</div>
1
colxi