4
3

Unityシェーダーのfmodの丸め誤差が謎

Last updated at Posted at 2024-08-14

Unityを使用してシェーダーを開発していると、ときおり思わぬ結果に直面することがあります。
本記事では、私がシェーダーで fmod 関数を使用した際に経験した「丸め誤差」による問題と、その対策について共有します!

fmod が正常に動かない!

シェーダーで以下のようなコードを書いたとします。

Shader "Segur/FmodErrorStudy/Unlit"
{
    Properties
    {
        _InputInt("InputInt", Int) = 5
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            int _InputInt;

            float4 vert(const float4 v:POSITION) : POSITION
            {
                return UnityObjectToClipPos(v);
            }

            fixed4 frag() : COLOR
            {
                // _InputInt は 5 なので、 5 * 5 は 25 になるはず。
                const float p1 = 5 * _InputInt;

                // 5 を 25 で割った余りは 5 になるはず。しかし実際には、 4.99999 のような値になっていると思われる。
                const float p2 = fmod(5, p1);

                // 5 を 5 で割った余りは 0 になるはず。しかし実際には、 4.99999 のような値になっていると思われる。
                const float p3 = fmod(p2, 5);

                // 計算結果を確認するため、 0~1 の範囲でRチャンネルに出力。結果が 0 なら黒色になるはず。
                // しかし実際には赤色になるため、 0.99999 のような値になっていると思われる。
                return fixed4(p3 / 5, 0, 0, 1);
            }
            ENDCG
        }
    }
}

理論上、このコードはサーフェスを黒色に描画するはずです。

しかし、実際には赤色が描画されるという問題が発生します。実際に試してみるとわかります。

image.png image.png
黒色で描画されることを期待 実際に描画されたのは赤色

原因は丸め誤差

この問題は、浮動小数点数の丸め誤差に起因していると思われます。
fmod 関数は、浮動小数点演算において誤差を生じやすく、期待される結果と実際の計算結果に微妙な差が生じることがあります。

コードの流れをフローチャートに書くと以下のようになります。

p2 = fmod(5, p1) の結果が 5 になり、その後の p3 = fmod(p2, 5) の結果は 0 になると期待しています。
しかし、実査には p30 ではなく、限りになく5に近い値(おそらく 4.99999など)になっていたため、赤色が描画されたと思われます。

round 関数で解決

この問題に対処するため、 round 関数を使用して四捨五入を行い、 fmod へ渡す前に値を正確な整数に丸める方法を採用しました。

fixed4 frag() : COLOR
{
    const float p1 = 5 * _InputInt;
    const float p2 = fmod(5, p1);
    const float p3 = fmod(round(p2), 5); // roundを追加
    return fixed4(p3 / 5, 0, 0, 1);
}

これにより、 fmod 関数による丸め誤差の影響を最小限に抑えることができ、期待通り黒色が描画されるようになりました!
round 関数は、浮動小数点の小さな誤差を取り除き、計算結果を安定させるのに非常に有効です。

image.png

意外な挙動 : プロパティを利用した計算は不安定?

ちなみに、 _InputInt というプロパティを使わずに、以下のように frag 関数内部で変数を初期化すると、 round を使わなくても期待通り黒色が描画されました!

fixed4 frag() : COLOR
{
    const int localInt = 5; // 関数内でint型の変数を初期化
    const float p1 = 5 * localInt; // _InputIntをlocalIntに変更
    const float p2 = fmod(5, p1);
    const float p3 = fmod(p2, 5);
    return fixed4(p3 / 5, 0, 0, 1);
}

なぜこれだとうまくいくのか謎です。

ここからわかることは、プロパティを利用した計算は想像以上に不安定なので、精度をそこまで信用できないということです。

さいごに

この記事が、同じような問題に直面した方の参考になれば幸いです。

本記事作成にあたり、以下のページを参考にさせていただきました。ありがとうございました!

4
3
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
4
3