撰写了文章 更新于 2017-12-15 18:05:14
【弹幕程序科普计划】 第七篇 帧率控制(里)
【帧率控制(里)】
在第五篇的时候,我介绍了在不使用垂直同步的情况下,有两种控制帧率的逻辑,分别是依据增量时间的传统方法以及依据绝对时间的改进方法。当时仅从理论上进行了讲解,然而,帧率控制方法实际操作起来其实非常需要技巧,尤其在计时器的操作方面存在诸多细节,仅凭先前说的那些理论,估计对于大多想自己写弹幕程序的人来说并不能起到实质性帮助。因此,本文将从代码逻辑方面,更进一步的讲解帧率控制方法。
之前说过,要控制帧率60fps最基本的方法就是每间隔16.667毫秒进行一次画面渲染。那么这里必然牵涉到计时问题,首先面对的问题当然也就是计时器的选用问题。相信不管使用什么编程语言,肯定都有一堆计时器可以选用,我比较熟悉的C#中常用的计时器就有三种,包括System.Windows.Forms.Timer、System.Timers.Timer、System.Threading.Timer。然而实际这些计时器都不可行,问题当然出在精度方面。这些计时器精度最高也只在毫秒级,然而要控制到16.667毫秒则精度必须达到微秒级。至于使用毫秒级计时器会产生多大误差,以下可以简单算一下。
当渲染间隔为16毫秒时
帧率 = 1/0.016 = 62.5fps
当渲染间隔为17毫秒时
帧率 = 1/0.017 = 58.8fps
因此,在选用毫秒级计时器来控制16.667这个数字时,帧率至少会在58.8fps至62.5fps间波动,比如无法获得稳定的60fps。
要获得稳定的60fps,就必须使用微妙级精度的计时器,其归根结底就只剩一种选择,就是依靠Win32API中的这两个方法:
QueryPerformanceFrequency 和 QueryPerformanceCounter
通过这两个方法可以获取CPU主频以及CPU跳动的次数,从而计算时间。这样的计时方法可以达到电脑所能达到的最高计时精度。
C#中对这两个方法倒是有托管封装,用起来会相对方便一些。该方法具**于System.Diagnostics名空间下的StopWatch类中,具体用法可以参见MSDN。如果使用其他语言的话,请参见WIN32 API。
在选定了计时器以后,我们来看下一个问题,关于定时器的实现方法。由于上述方法只能实现对当前时间的访问,并不能够实现定时触发,想要定时发出中断信号依然不能直接实现。换个通俗的说法,以上方法只能告诉你当前时间是多少,却并没有每隔16.667毫秒发出一个信号这样的功能。要实现每间隔16.667毫秒渲染一次画面,最简单粗暴的写法是使用循环查询。在程序中不断地读取当前时间,并判定是否达到指定时间,如果达到的话进行画面渲染,没达到的话继续读取时间。这种最简单粗暴方法的程序框图如下所示:
以上方法相当于在利用读取时间的代码来进行延时,虽然定时精度高,但是相当占用CPU资源,因为CPU始终在读取时间。显然这种方法会平白无故浪费CPU,并不是很可取。然而,如果通过降低查询的频率来节省CPU的话,必然导致定时精度降低,因此这里程序的写法上其实要有一点技巧,可以通过先延时再查询的方法。
各种编程语言基本都会有个方法叫Sleep,可以用于延时。当使用Sleep方法时,其实是将线程挂起一段时间,并不占用CPU资源,然而其精度也只在毫秒级,不能直接用于帧率控制。不过,这里我们却可以利用Sleep方法来降低CPU的负担。当一帧画面渲染完毕后,先查询一下当前时间,得到距下一次渲染画面还差多少时间,然后依据此时间进行Sleep延时。举例来说,查询发现距离下一次渲染画面的时间还有11.5毫秒,那么可以先Sleep延时个11毫秒,然后进入循环查询模式,来保证定时精度,这样,相当于去掉了11毫秒的CPU负荷,仅有0.5毫秒处于循环查询模式。此方法的程序框图如下:
用此方法,可以在确保帧率控制精度的前提下,大大降低CPU负担。此外,以上框图所示的是传统增量式的帧率控制方法,如果要使用绝对帧率控制法,则仅在第一帧时进行计时器清零,之后将判断“是否到达16.667毫秒”改为“当前时间是否到达16.667毫秒的整数倍”即可。
以上便是今天关于帧率控制的科普。
根正苗红而且不吃铁皮的高铁默默注视着你 1年前
发布