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

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


# `fmod` が正常に動かない！

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

```hlsl
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/aecf9397-1351-84f1-fcdd-261f63eba3b2.png) | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/cc1af409-5bd2-0bd6-6278-842e342431cb.png) |
|:---:|:---:|
| 黒色で描画されることを期待 | 実際に描画されたのは赤色 |


# 原因は丸め誤差

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

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

```mermaid
graph TD
    A["_InputInt = 5"] --> B["p1 = 5 * _InputInt"]
    B -->|期待値: p1 = 25, 実際: ？| C["p2 = fmod(5, p1)"]
    C -->|期待値: p2 = 5, 実際: p2 ≈ 4.99999| D["p3 = fmod(p2, 5)"]
    D -->|期待値: p3 = 0, 実際: p3 ≈ 4.99999| E["Rチャンネルに p3 / 5 を出力"]
    E -->|期待値: 0, 実際: ≈ 0.99999| F["黒色ではなく赤色が描画される"]
```

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



# `round` 関数で解決

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

```hlsl
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/aecf9397-1351-84f1-fcdd-261f63eba3b2.png)


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

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

```hlsl
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);
}
```

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

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


# さいごに

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

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

- https://wiki.amplify.pt/index.php?title=Unity_Products:Amplify_Shader_Editor/Fmod