LoginSignup
18
10

【Unity】時間経過によるシェーダーの_Timeの精度低下を検証

Last updated at Posted at 2022-07-18

はじめに

経過時間の計算など、加算し続ける変数をfloat型にしてしまうと、結構すぐに意図しない挙動を引き起こすという話があります。
値が大きくなると分解能が下がるので、そこに小さな値を足しても情報落ちが発生してしまうのです。
詳しくはこちらの記事などを参考にしてください。

シェーダーではどうなる?

スクリプトではdouble型にすれば良いですが、シェーダーの場合はそういうわけにはいきません。
Unityシェーダーの組み込み変数である_Timeからはシーンロード時点からの経過時間が取得できますが、float型です。
同じシーンを長時間プレイしていると、この値はどうなるのでしょうか?
恐らくスクリプトのdouble型の変数からシェーダーに値を渡しているので、値が大きくなっても情報落ちにはならないと推測できます。
しかし、分解能は下がっていくので、だんだんと動きがカクカクしていくことが予想されます。

予想だけでは自信を持って語れないので、検証することにしました。

floatの分解能の計算にはこちらのサイトを活用しました。

検証内容

検証用シーンを2種類作りました。
このシーンを暫く実行し続け、どのような変化が生じるかを観察します。

1:時計パターン

経過時間の値を参照して時計の1秒タイマーの針を動かします。

  • 左:シェーダーで _Time.y を利用して針を回す
  • 中央:スクリプトで Time.timeSinceLevelLoadAsDouble を利用して針を回す
  • 右:スクリプトで Time.deltaTimeをfloat型の変数に加算し、 累積値を利用して針を回す

今回の検証目的とは異なるのですが、 Time.deltaTime の累積も精度が低いという話があるので、併せて検証することにしました。

右下の数値はそれぞれ以下の値です。

  • Elapsed Seconds (DeltaTime)
    • Time.deltaTime の累積(float型)
    • 右側の時計の針を動かすのに利用している値
  • Elapsed Seconds (float)
    • Time.timeSinceLevelLoad の値
    • 左側の時計の針を動かすのに利用しているシェーダーの _Time.y と同じ値と思われる
  • Elapsed Seconds
    • Time.timeSinceLevelLoadAsDouble の値
    • 中央の時計の針を動かすのに利用している値

2:UVスクロールパターン

_Time を利用し、シェーダーのみでUVスクロールを行います。
毎フレーム以下の量スクロールさせます。

  • 左: frac(_Time.y) * 1.5
  • 右: frac(_Time.w) / 2

検証結果

1時間後:deltaTime累積時計が遅れだす

右側のdeltaTimeの累積で針を回している時計が遅れるようになりました。

右下の Elapsed Seconds (DeltaTime) の値を見ると他の値より小さいです。
毎フレームdeltaTimeを加算するときに分解能以下の小さな値は切り捨てられる1ことになりますが、その切り捨てられた分が積もった結果だと思われます。

累積ではなく、 deltaTime の値の分だけ時計の針を回すというパターンも試してみたところ、この場合は数時間動かしても遅れは生じませんでした。
このため deltaTime の精度が低いわけではなく、加算が遅れの要因となっていると考えられます。

3日経過後:deltaTime累積時計が倍速になる&UVスクロールがカクつきだす

経過時間が262,144秒(約3日)に達すると、floatの分解能(1bitと対応する値)が0.016から0.031になります。
分解能0.031_10進数→IEEE754.png 分解能0.031_IEEE754→10進数.png

時計パターン

このタイミングで右側のdeltaTimeの累積で針を回している時計の動きが倍速になりました。

想定外の挙動であったため調査したところ、丸め誤差によるものであることがわかりました。

UVスクロールパターン

UVスクロールの方にも変化が現れました。
検証開始時点のものと比べると、僅かに滑らかさが減少しました。
右側の方が_Time.w(経過時間*3)を参照しており値の増加スピードが速いので、影響が大きく出ています。

6日経過後:deltaTime累積時計停止

経過時間が524,288秒(約6日)に達すると、floatの分解能が0.031から0.063になります。
分解能0.063_10進数→IEEE754.png 分解能0.063_IEEE754→10進数.png
_Time.y1秒 / 0.063 ≒ 15fps となります。

時計パターン

右側のdeltaTimeの累積で針を回している時計が停止しました。
左側のシェーダーで針を回している方も滑らかさがなくなりました。

右下の Elapsed Seconds (DeltaTime) も増えなくなっています。
分解能が下がったことにより、毎フレーム約0.016を加算しても丸め処理で切り捨てとなり、情報落ちが発生して累積が増えなくなったということです。
完全に停止しているわけではなく、フレームレートが下がったと思われるときに急に動きます。

UVスクロールパターン

UVスクロールはカクカクしているのがはっきりとわかるようになりました。

10日経過するとここまでカクカクするようになりました。

12日経過後:8fps

経過時間が1,048,576秒(約12日)に達すると、floatの分解能が0.063から0.125になります。
分解能0.125_10進数→IEEE754.png 分解能0.125_IEEE754→10進数.png
_Time.y1秒 / 0.125 = 8fps となります。

時計パターン

左側のシェーダーで針を回している時計は8fpsになったので、きれいに1/8ずつ回るようになりました。

UVスクロールパターン

こちらはカクカクというよりガタガタになってきました。

24日経過後:4fps

経過時間が2,097,152秒(約24.3日)に達すると、floatの分解能が0.125から0.25になります。
分解能0.25_10進数→IEEE754.png 分解能0.25_IEEE754→10進数.png
_Time.y1秒 / 0.25 = 4fps となります。

4fpsになったので、左側のシェーダーで針を回している時計はきれいに1/4ずつ回るようになりました。
右下の Elapsed Seconds (float) の値も0.25ずつ増えているのがわかります。

38日経過後:UVスクロール停止

右側の _Time.w を参照している方はスクロールが停止しました。
左側の _Time.y を参照している方も1回の動きの幅が大きくなり、右上から左下にスクロールしていると認識するのが難しくなりました。

おわりに

理論上一週間もしないうちに影響が出るはずと考えて検証を開始しましたが、deltaTimeの累積が倍増した以外は予想通りの結果となりました。
ただこうなると、シェーダーで _Time を参照してアニメーションさせるのはかなりリスクが高いです。
アニメーションの速度がゆっくりであれば影響を感じるまでの期間も長くなりますが、それでもいつかはカクつきだします。
時間制限がありシーンが切り替わるタイミングが決まっているシーン以外では、ユーザーが同じシーンを何日間も開いたままにしておく可能性を捨て切れません。
シェーダーのみでアニメーションできるとスクリプトと疎結合になって便利なのに、これが使えないというのは残念ですね。

  1. 正確には最近接丸め。もっと正確には端数処理の方法は環境によって異なります。

18
10
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
18
10