Search Unity

皆様は Unity に正確なフレームレートを維持させたり、さらには(一般的にGenLockとして知られる)外部のクロックソースに従わせたりできないかと思われたことはありませんか?本記事では、Unity がネイティブレベルでフレームレートをどのように維持しているかをご説明するとともに、それを厳密に制御するためにユーザーコードを追加する方法をご紹介します。これは、Unity と他の機器との同期が不可欠となる放送スタジオなどでは、特に重要な知識です。

通常、デフォルトの状態の Unity プロジェクトは、プロジェクトをできるだけ速く実行しようとします。フレームは、基本的にディスプレイデバイスのリフレッシュレートの制約を受けながらも、可能な限り速くレンダーされます(V Sync Count プロパティの説明 をご参照ください)。最も簡単にフレームレートの制御を開始する方法は、QualitySettings.vSyncCount を明示的に設定し、ディスプレイデバイスのリフレッシュレートに適した間隔でレンダリングが行われるようにすることです(例えば、60Hz のディスプレイの場合、vSyncCount=2 と設定すると Unity が 30 FPS でレンダーするようになり、ディスプレイと同期される形になります)。しかし、この方法だとディスプレイのリフレッシュレートの約数に制約されるため、十分に精度の高い制御にはならないかも知れません。

上記の次に簡単なソリューションは、QualitySettings.vSyncCount=0 を設定し、Application.targetFrameRate を使用してディスプレイのリフレッシュレートから独立したフレームレートをターゲットとして設定する方法です。この設定を行うと、レンダリングループがこのレートの近似値に抑制されます(ただし、Unity のレンダリングがディスプレイと同期された形で行われなくなるのでティアリングが発生する可能性があることにご注意ください)。これは、CPU 資源の必要以上の消費を避けるために低コストな方法で行われます。このアプローチのマイナス面は、すべてのユースケースで必要な精度が確保されるとは限らないことです。

しかし、コルーチンによって精度を向上させることが可能ですので、ご心配には及びません。Unity 組み込みのフレームレートスロットリングに頼るのではなく、それをスクリプトコード経由でご自分で制御していただくことができます。そのためには、Unity が出来るだけ高速で実行しようとするように設定する必要があります。QualitySettings.vSyncCount=0 を設定し、Application.targetFrameRate を非常に高い値に設定した上で、WaitForEndOfFrame コルーチンを使用して指定の時点まで次のフレームがレンダーされないようにすることで、目標レートぴったりまで落とすことができます。これを正確に行うためには、Thread.Sleep の組み合わせを使用して CPU 資源を無駄に消費することなく低コストで Unity のレンダリングループを遅延させた上で、最後の数ミリ秒で、次のフレームを開始させたい正確なタイミングを確認しながら CPU をスピンさせることをお勧めします。Unity のフレームレートを外部のクロック(GenLock)と連携させたい場合は、外部の同期信号が受信された時点で即座にこの CPU スピンループから脱出する必要があります。

最後にこれに関連して、時間を外部のクロックソースに合わせたい場合について補足します。この場合、Unity 内部のゲーム時間も(徐々に外部ソースからずれる可能性がある CPU のクロックに従うのではなく)外部のクロックソースに合わせて進めたいところです。これは、Time.captureFramerate を外部クロックと同じレートに設定することで行えます。例えば、外部の GenLock の信号が 60 FPS で実行されている場合、captureFramerate を 60 に設定すれば、(実際に経過した時間の長さに関わらず)ひとつのフレームのレンダリングと次のフレームのレンダリングの間にゲーム時間のぴったり 60 分の 1 秒を経過させるように Unity に命令が送られます。Unity 2019.2 ベータ版では、Time.captureDeltaTime を使用することで、キャプチャーフレームレートに浮動小数点値を設定できるようになっています。これより古いバージョンの Unity では、外部の信号が整数ベースのレートで実行されていない場合(59.94 FPS になっているなど)は、各フレームのレンダリングごとに captureFramerate を変えることで、時間の進行に関して目標の平均レートを実現することができます。

上記のトピックを実際に試した概念実証のための Unity のサンプルプロジェクトが GitHub プロジェクトページからご入手いただけます。具体的には ForceRenderRate.cs に、WaitForEndFrame コルーチンを使用したフレームレートの正確な制御の例が提供されています。外部の GenLock のエミュレートよりも複雑な例は GenLockedRecorder.cs でご確認いただけます。Unity はネイティブではベンダー別の GenLock 実装には対応していませんが、後者の例はこの機能を提供するサードパーティー SDK の統合を開始するための足掛かりとしてお役立ていただけます。

(注)この記事で紹介したすべてのテクニックは、Unity のスタンドアロンプレイヤービルドの一部として使用された場合に最も安定したフレームレートを実現します。これらは Unity エディターの再生モードでも機能しますが、ときどき一時的な変動が起こる可能性もあります。

16 コメント

コメントの配信登録

コメント受付を終了しました。

  1. It’s very odd that, in an article about precise frame rate (and thus: timings), there’s no mention of THE class provided by C# to do exactly that: System.Diagnostics.Stopwatch.

    Seriously, whenever precise timing measures are needed, Stopwatch is the way to go. And yes, it’s perfectly fine to use it in runtimes (sometimes I bump into people that believe that, since the namespace it’s called Diagnostics, then it’s not advisable to use it in production code: this is wrong).

  2. Unity has a serious problem with the method used for calculating Time.deltaTime, which causes frame stuttering in many system/devices, no matter what you do. The solution in this article seems to provide a way to have a better timing, but at the cost of experiencing tearing (no vsync). Not working properly in the Editor is also a con.

    This is the cause of the frame stuttering problems in Unity: an ancient issue that other engines have resolved somehow, but Unity has left behind so it’s now a major issue:
    https://medium.com/@alen.ladavac/the-elusive-frame-timing-168f899aec92

    Summarized and explained:
    https://forum.unity.com/threads/time-deltatime-not-constant-vsync-camerafollow-and-jitter.430339/page-4#post-4173886

    Once the problem is understood, I’ve been able to get perfectly smooth 60 fps in Unity (both builds and Editor) with this:

    – Set Time.captureFrameRate = 60
    – Force VSync enabled externally (display’s control panel, or opening Unity in OpenGL mode), as Time.captureFrameRate disables VSync.

    The above solution causes Time.deltaTime to be exactly 1/60 seconds each frame. VSync forced means each frame is displayed at its correct time. Result is a perfectly smooth motion in both Editor and builds.

    It’s not a generic solution as it causes other issues (i.e. no frames are dropped ever, no matter the CPU/GPU load), but it demonstrates the nature of the problem. It won’t get magically resolved by itself unless Unity devs decide to really understand and tackle it directly. Time.deltaTime must be computed based on the frame display rate, not in the frame rendering rate as is now.

  3. Glad to see Unity’s Time is being looked at. Many of us have been fighting visual jitter issues for many years due to Time.deltaTime variation (even when lightly loaded and when frames aren’t dropped). Will these changes fix this issue?
    https://forum.unity.com/threads/time-deltatime-not-constant-vsync-camerafollow-and-jitter.430339/

  4. realtimeSinceStartup precision may be lacking as tied to the system : https://docs.unity3d.com/ScriptReference/Time-realtimeSinceStartup.html .
    Moreover, it’s a float and thus the precision mantissa is 23 bits.
    This means that after having run for ~2.5 hours, the precision will be under 1ms.
    To achieve a smooth 60fps, you need to wait 16.67ms between each frame. With a 1ms precision, it’s not possible, so after ~1 hour you may already notice some small frame hiccup.

    To get an accurate time counter, you need to maintain your own double timer, incremented by Time.unscaledDeltaTime at each update(). (you can also use DateTime.now but its behavior seems sketchy when needing relatively high precision, possibly only on multi-core CPUs ; StopWatch is another solution that I didn’t check in depth for such goal)

    It’d be better if you Unity could provide realtimeSinceStartup as a double… :-)

  5. No, GenLock is just a clock source to the GPU(s) usually via a sync card (e.g. NVidia Quadro SyncII). Causing the scanline across all output display(s) to occur at the same time at the clocked frequency. Unity needs to have a fully rasterized frame presented (for all outputs) before this clocked rate (VSync). Failure to do so will result in a dropped frame (very bad in broadcast/film).

  6. Hi.
    1. Why does setting targetFrameRate may not yield the required precision?

    2. What happens if you don’t “spin the CPU while checking for exactly the right time to allow the next frame to start” and sleep for as long as you need? (currentTime – t)

    3. If you run that on iOS you will cause your game to run at 30fps instead of 50, right? That is, this is not meant to be used in iOS….. ?

    1. I have these questions, too!

  7. Would be nice if you could set the physics fps or set it to work to the frame rate. Currently you have a timestep with default 50 fps (.02) and if you want it at 60fps you have to put in a craxy fraction 1/60. Sure you can interpolate physics for dispay but would it not be faster to have a core fps same as game tick or refresh rate?

    1. The case for this is actually a lot weaker than most people think.

      Physics, for instance, doesn’t actually do a whole lot at 60 Hz. By that, I mean there’s not much that naturally occurs or needs to be checked every 16.66ms. For racing games, it’s higher, typically in the hundreds of Hz to catch the rumble strips and fine detail when you’re moving ~100m/s. On the other hand, for first-person games, collisions between players, objects, props isn’t so rapid, so 50, 40 or even 25Hz can work. I’ve seen one even get away with 12.5 Hz physics timesteps, and nobody noticed.

      As for 50 Hz physics on a 60 Hz display, the difference is between 20ms and 16.66ms, so about 3.4ms. So, assuming you could un-align things, one object could move through another about 3.4ms long than it should have under ideal conditions. Think of it as playing an online game with position extrapolation and a ping of 3.4, you would barely notice it.

      The case for 60, as in the same as the framerate, could be made for things, yes, but if you’re basing everything on screen framerate (so a 144Hz monitor would use 144Hz physics), you already have Update to work with for these sort of things.

      1. Physics isn’t the only thing that fixedupdate has a use for.

        1. Robert Cummings

          6月 16, 2019 3:06 pm

          If the developer is lazy, then FixedUpdate can be used for non physics purposes, yeah. All it really does though is run before Update if it’s required. You can absolutely replicate it with your own timer in a couple of lines of code. It doesn’t run parallel or anything, it runs before Update 0 to n times per frame.

          https://docs.unity3d.com/Manual/ExecutionOrder.html

          Having said that, if your game is physics based and gameplay or anims rely on physics being in place then sure – put the code in FixedUpdate.

          Don’t do it if you’re not using physics or physics-dependent code though, you’re just introducing potential for things to go wrong if inexperienced. If experienced then… well you wouldn’t be using it for a purpose it’s not made for.

  8. I’m not sure co-routines really are the best model for doing things like this either. They generate garbage and cause frame-hitches, however with the new GC collector should be reasonable. In a larger older project though the GC might stall, if it’s been running for a long time (5 mins or so?). But the information in the blog is more than enough for people to roll their own timings if they wish, thanks for the contribution.

  9. Wolfram Kresse

    6月 3, 2019 6:10 pm

    Note that WaitForEndOfFrame() is quite unreliable in the Editor, as it also triggers on Game window redraws caused by the Editor. I tried to bug this in the past, but the answer was that this is the correct behaviour by design…
    From the bug report (1077480): “This is the intended behavior. Every function like OnGUI(), WaitForEndOfFrame() is rendering-dependent code, so it gets called when the editor renders the game view, even if it’s paused.”

  10. Thread.Sleep do not guarantee any precision. It will wake your thread back after no less than given interval, but not exactly. Check out the remarks here https://docs.microsoft.com/en-us/windows/desktop/api/synchapi/nf-synchapi-sleep

  11. Interesting, the only reason I would want to do this is on mobile… however the problem with doing this, is that input handling especially with touch becomes really awful for interaction and responsiveness… Am hoping the new input system stuff is going to be looked into with a more official built in way of doing this better for battery life on mobile with Unity based projects that aren’t exactly games… because frankly doing multiplatform using any else is just rubbish I found.. C# cross platform is the sweet spot but without any decent cross platform GUI frameworks its real the problem, and well uGUI despite being rather lackluster on features is better than the hassle of anything else I found.

  12. Alejandro Castan

    6月 3, 2019 5:09 pm

    Very intesting topic for VR projects !!! Thanks Unity