【Unity】【シェーダ】ゼロのゼロ乗は「1、あるいは未定義」だと言うことを実感した話

Unityのシェーダでゼロのゼロ乗は「1、あるいは未定義」だと言うことを実感した話をまとめました。

Unity2020.3.15f2
URP 10.5.1

pow関数の不可解な挙動

先日シェーダを書いていて、pow関数の不可解な挙動を目の当たりにしました。

これを説明するために、まず以下のように普通にゼロのゼロ乗を計算してみます。

half4 frag() : SV_Target
{
    // testはコンパイル時点で1となる
    half test = pow(0.0, 0.0);
    return float4(test, 0, 0, 1);
}

このシェーダをアサインしたオブジェクトは赤く出力されます。
すなわち、ゼロのゼロ乗は1として計算されていることがわかります。
コンパイル結果(D3D)を見てもtestの値は1として計算されていることがわかります。

void main()
{
    SV_Target0 = vec4(1.0, 0.0, 0.0, 1.0);
    return;
}

さて次に、累乗の指数にプロパティを使ってみます。
計算するのは同じくゼロのゼロ乗です。

float _test = 0;

half4 frag() : SV_Target
{
    // testはコンパイル時点でゼロになる
    half test = pow(0.0, _test);
    return float4(test, 0, 0, 1);
}

このシェーダをアサインしたオブジェクトは黒く出力されます。
すなわち、ゼロのゼロ乗の計算結果がゼロとなっていることがわかります。
コンパイル結果を見てもtestの値は0として計算されています。

void main()
{
    SV_Target0 = vec4(0.0, 0.0, 0.0, 1.0);
    return;
}

ゼロのゼロ乗は1あるいは未定義である

さてなぜこのようなことが起こるのでしょうか。

僕の拙い数学の知識ではゼロのゼロ乗は1になるものだとばかり思っていましたが、
厳密には「1と定義されることもあるし未定義とされることもある」ようです。

その値は、代数学、組合せ論、集合論などの文脈ではしばしば 1 と定義される[注 1]一方で、解析学の文脈では二変数関数 xy が原点 (x, y) = (0, 0) において連続とならないため定義されない場合が多い。

https://ja.wikipedia.org/wiki/0の0乗 より

ではコンピュータにおいてはどうなのかというと、これもまた言語により1としている場合もあれば未定義としているものもあるようです。

いくつかのプログラミング言語は 00 を定義しており、その多くは 1 としている。1 と定義しているプログラミング言語は、Java, Python, Ruby, Perl, Haskell, Common Lisp, ML, Scheme, R, MATLAB, Julia, APL, J であり、電卓では、Microsoft WindowsおよびGoogleの電卓機能[12]などである。Microsoft Excel では、ワークシート上で =00 という数式を入力すると #NUM! というエラーを返すが、同ソフトウェアに搭載されている VBA では1と定義されている[注 7]。 Mathematica は、a が変数または 0 でない数のときは a0 を 1 と計算するが、00 は Indeterminate(不定)と返す。Maple やMuPADはこれらを共に 1 と計算する。Wolfram Alpha ではundefinedと表示される。 https://ja.wikipedia.org/wiki/0の0乗#コンピュータにおける扱い より

どうすればいいのか

このように、ゼロのゼロ乗は1になることもあるし未定義とされることもあると言う事実があります。
冒頭の例についても内部的な処理はわかりませんが、これに起因する現象であると考えられます。

このようなことから、そもそもゼロのゼロ乗になるような処理は避けた方が良さそうです。
仕様によりますが、例えば以下のように値の範囲を制限すれば、不要なバグを踏まなくて済みます。

float _test = 0;

half4 frag() : SV_Target
{
    half test = pow(0.0, max(0.0001, _test));
    return float4(test, 0, 0, 1);
}

参考

ja.wikipedia.org