0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

60 FPS にする時に各フレームは何秒にするのが理想的かを計算する

Last updated at Posted at 2024-07-24

その昔、Win32 でゲームを作ってた頃に timeGetTime() と sleep() で正確に 60 FPS にするには各フレームでどれだけ時間をとればいいのか考えた事があった。
(ちなみに、確か Direct2D に画面フリップまでブロックする処理があって、そっちを使った方が画面は崩れなかった気がする)

今は可変 FPS の時代だし、あんまり意味はなさそうだけど、なんかの参考になるかもしれないのでメモっておく。

大枠の考え方としては「きれいにバラけるように事前に表を作っておいて、各フレームではその表の時間だけ使っていいとする」みたいな感じ。

その表を作る部分は当時「こういう表が欲しいわけだから……」というところから逆算して、これでいけるな!と思った方法という、説明しがたいやつだったが、今回 HTML にべた書きした JavaScript でその表を計算して表示するので、実行して存分に堪能して欲しい。

ideal-frame-duration-calculation.html
<html>
    <head>
        <title>ideal frame duration calculation</title>
        <script>
(function(global){
    // 二つの数の最大公約数 (greatest common divisor) を求めます.
    const gcd2 = function(value1, value2) {
        if ((value1 == 0) || (value2 == 0)) {
            // 想定外の引数
            throw "illegal argument";
        }
        
        // ユーグリッドの互除法 (Euclidean Algorithm)
        while (value2 != 0) {
            const tmpValue = value2;
            value2 = value1 % value2;
            value1 = tmpValue;
        };
        
        return value1;
    };
    
    // // 二つの数の最小公倍数 (least common multiple) を求めます.
    // const lcm2 = function(value1, value2) {
    //     
    //     if ((value1 == 0) || (value2 == 0)) {
    //         // 想定外の引数
    //         throw "illegal argument";
    //     }
    //     
    //     const v1 = value1 * value2;
    //     const v2 = gcd2(value1, value2);
    //     const result = parseInt(v1 / v2);
    //     return result;
    // };
    
    // `createCycleTable` 理想 FPS の為に各フレームで待つべき時間の表を作成します.
    // `waitsPerSecond` 一秒辺りの待ち時間 (ミリ秒単位なら 1000, マイクロ秒単位なら 1000000)
    // `fpsToDesire` 理想秒間フレーム数
    const createCycleTable = function(waitsPerSecond, fpsToDesire) {
        const baseTime = parseInt(waitsPerSecond / fpsToDesire);
        const remaindered = waitsPerSecond % fpsToDesire;
        if (remaindered == 0) {
            return [baseTime];
        }
        
        const gcdValue = gcd2(fpsToDesire, remaindered);
        const cycleItemCount = parseInt(fpsToDesire / gcdValue);
        const longItemCount = parseInt(remaindered / gcdValue);
        
        var cycleTable = [];
        const mag = 65536;
        const toDecrease = parseInt(mag * longItemCount / cycleItemCount);
        var counter = mag * longItemCount;
        for (var i = 0; i < cycleItemCount; i += 1)
        {
            const prev = parseInt(counter / mag);
            counter -= toDecrease;
            const curr = parseInt(counter / mag);
            if (prev != curr) {
                cycleTable[i] = baseTime + 1;
            } else {
                cycleTable[i] = baseTime;
            }
        }
        
        // 確かめ算 verification of figures
        var cntForVerification = 0;
        var pos = 0;
        for (var i = 0; i < fpsToDesire; i += 1) {
            cntForVerification += cycleTable[pos];
            pos += 1;
            pos %= cycleItemCount;
        }
        
        if (cntForVerification != waitsPerSecond) {
            throw `verification failed. expects: ${waitsPerSecond}, but actual: ${cntForVerification}`;
        }
        
        return cycleTable;
    };
    
    document.addEventListener('DOMContentLoaded', function() {
        const waitsPerSecondBox = document.querySelector('#waits-per-second-box');
        const fpsToDesireBox = document.querySelector('#fps-to-desire-box');
        const calcTableButton = document.querySelector('#calc-table-button');
        const resultPane = document.querySelector('#result-pane');
        
        
        calcTableButton.addEventListener('click', function(event){
            const waitsPerSecond = parseInt(waitsPerSecondBox.value, 10);
            const fpsToDesire = parseInt(fpsToDesireBox.value, 10);
            const cycleTable = createCycleTable(waitsPerSecond, fpsToDesire);
            const cycleItemCount = cycleTable.length;
            
            // 結果の表を作る (ここは出りゃいいでしょという適当なやり方)
            const tableElm = document.createElement('table');
            for (var i = 0; i < cycleItemCount; i = i + 1) {
                const cycleItem = cycleTable[i];
                
                const td1Elm = document.createElement('td');
                td1Elm.innerText = `${i + 1}`;
                const td2Elm = document.createElement('td');
                td2Elm.innerText = `${cycleItem}`;
                const trElm = document.createElement('tr');
                trElm.appendChild(td1Elm);
                trElm.appendChild(td2Elm);
                tableElm.appendChild(trElm);
            }
            resultPane.innerHTML = '';
            resultPane.appendChild(tableElm);
            
            tickCycleTable = cycleTable;
            tickCycleTableIndex = 0;
        });
        
        onTick();
    });
    
    const getTickCount = function() {
        return (new Date()).getTime();
    };
    
    var progress = 0;
    var increment = 15;
    var max = 360;
    
    var tickCycleTable = [1000];
    var tickCycleTableIndex = 0;
    var tickCount = getTickCount();
    const onTick = function() {
        const thisTickStartedAt = getTickCount();
        const interval = tickCycleTable[tickCycleTableIndex];
        tickCycleTableIndex += 1;
        tickCycleTableIndex %= tickCycleTable.length;
        // console.log(tickCycleTableIndex);
        const nextTickShouldStartAt = tickCount + interval;
        tickCount += interval;
        // ----
        
        const hoge = Math.sin(progress * (Math.PI / 180));
        
        const demoCanvas = document.querySelector('#demo-canvas');
        const ctx2d = demoCanvas.getContext('2d');
        ctx2d.fillStyle = 'white';
        ctx2d.fillRect(0, 0, 100, 100);
        
        ctx2d.fillStyle = 'blue';
        ctx2d.beginPath();
        ctx2d.arc(50, 50 + (10 * hoge), 20, 0, Math.PI * 2, true);
        ctx2d.closePath();
        ctx2d.fill();
        
        // console.log(`${progress} ${mage}`);
        
        progress += increment;
        progress %= max;
        
        // ----
        const tickProcessEndedAt = getTickCount();
        var delay = nextTickShouldStartAt - tickProcessEndedAt;
        if (delay < 0) {
            console.log('間に合わない');
            tickCount = tickProcessEndedAt;
            delay = interval;
        }
        
        setTimeout(onTick, delay);
    };
})(this);
        </script>
        <style>
            table { border: 1px solid; }
            td { border: 1px solid; }
        </style>
    </head>
    <body>
        <div>
            <label for="waits-per-second-box">Waits per second</label>
            <input id="waits-per-second-box" type="text" id="" value="1000" />
            <span>一秒辺りの待ち時間 (ミリ秒単位なら 1000, マイクロ秒単位なら 1000000)</span>
        </div>
        <div>
            <label for="fps-to-desire-box">FPS to desire</label>
            <input id="fps-to-desire-box" type="text" id="" value="60" />
            <span>理想秒間フレーム数</span>
        </div>
        <div>
            <button id="calc-table-button">Calculate</button>
        </div>
        <div>
            <label for="demo-canvas">demo</label>
            <canvas id="demo-canvas" width="100" height="100"></canvas>
        </div>
        <div id="result-pane">
        </div>
    </body>
</html>

「数学は無理、算数の時点で超苦手」という自分が何故最大公約数にたどり着いたのか、本当に覚えてない。
(でも一人でゼロから売り物の 3D のアクション RPG (BREW by KDDI な奴のアプリ) を書いた事あるんですよ!描画はライブラリがやってくれたけど、内部計算はガチで自力で全部やったの!ってか malloc/free 相当のやつも自作した。自分でも信じられない!)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?