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
}
}
}
理論上、このコードはサーフェスを黒色に描画するはずです。
しかし、実際には赤色が描画されるという問題が発生します。実際に試してみるとわかります。
黒色で描画されることを期待 | 実際に描画されたのは赤色 |
原因は丸め誤差
この問題は、浮動小数点数の丸め誤差に起因していると思われます。
fmod
関数は、浮動小数点演算において誤差を生じやすく、期待される結果と実際の計算結果に微妙な差が生じることがあります。
コードの流れをフローチャートに書くと以下のようになります。
p2 = fmod(5, p1)
の結果が 5
になり、その後の p3 = fmod(p2, 5)
の結果は 0
になると期待しています。
しかし、実査には p3
が 0
ではなく、限りになく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
関数は、浮動小数点の小さな誤差を取り除き、計算結果を安定させるのに非常に有効です。
意外な挙動 : プロパティを利用した計算は不安定?
ちなみに、 _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);
}
なぜこれだとうまくいくのか謎です。
ここからわかることは、プロパティを利用した計算は想像以上に不安定なので、精度をそこまで信用できないということです。
さいごに
この記事が、同じような問題に直面した方の参考になれば幸いです。
本記事作成にあたり、以下のページを参考にさせていただきました。ありがとうございました!