概要
UE5のNiagaraでパーティクルをアニメーションさせる手法の一つとして、Particle SpawnにScratchPadモジュールを置き、その中で以下のようなカスタムHLSLを書いて初期位置を決めるみたいなやり方があります。
float angle = ID * 2.0 * 3.14;
pos = center + 100 * float3(cos(angle), sin(angle), 0.0);
IDはインプットピンに設定している値でそこにNiagaraのUniqueIDを渡します。UniqueIDはエフェクトのプレイタイムの中で全てのパーティクルに対して一意に与えられるint32型の整数です。このようなパーティクルを短いライフサイクルで生成破棄することで、UniqueIDをどんどん更新していき、アニメーションさせるといった手法です。
しかしここで疑問に思ったことが、そういえば三角関数って『2nPI』で一周しなかったか?ということです。
つまりUniqueIDは整数なのでangleは常に2nPIの倍数になるので常に同じ場所にとどまり続けるのではないか?ではなぜアニメーションしてるのか?と。
考察
三角関数は2PIで一周するので2PIでfmod(浮動小数点の剰余算)してみます。レイマーチングとかでよく使う値をある値を閾値に繰り返すみたいな関数ですね。
fmodは以下のように計算します。
float fmod(float x, float y)
{
float n = floor(x / y);
return x - n * y;
}
まずPIを3.14としてfmodしてみます。
fmod(2.0 * 1.0 * 3.14, 2.0 * 3.14) = 0.0
fmod(2.0 * 2.0 * 3.14, 2.0 * 3.14) = 0.0
fmod(2.0 * 3.0 * 3.14, 2.0 * 3.14) = 0.0
fmod(2.0 * 4.0 * 3.14, 2.0 * 3.14) = 0.0
fmod(2.0 * 5.0 * 3.14, 2.0 * 3.14) = 0.0
fmod(2.0 * 2000.0 * 3.14, 2.0 * 3.14) = 0.0
この場合の計算結果は常に0になり、確かに2nPIで一周してその場にとどまっていることになります。
でもちょっと待ってください。3.14、この数値は円周率のほんの一部でしたよね?・・・そう、円周率は 3.141592653589........ と無限に続く無限小数でした。
https://www.tstcl.jp/randd/constants/pi/
ここで計算の便宜上、今度はPIを3.1415として再計算してみます。ただしangleに渡すコードは『2.0 * ID * 3.14』と3.14のまま計算します。
すると
fmod(2.0 * 1.0 * 3.14, 2.0 * 3.1415) = 6.280
fmod(2.0 * 2.0 * 3.14, 2.0 * 3.1415) = 6.277
fmod(2.0 * 3.0 * 3.14, 2.0 * 3.1415) = 6.274
fmod(2.0 * 4.0 * 3.14, 2.0 * 3.1415) = 6.271
fmod(2.0 * 5.0 * 3.14, 2.0 * 3.1415) = 6.268
fmod(2.0 * 2000.0 * 3.14, 2.0 * 3.1415) = 0.283
nが進むにつれて計算結果がずれていきます。2000ぐらいにするとその差は顕著ですね。
(https://keisan.casio.jp/calculator で計算)
実際にangleをsin関数に使ってグラフにするとこんな感じです(iq先生のGraphtoyで描画)
結論
HLSL側でPIを3.14と端折っていてもsin関数的にはPIは無限小数なので定数のUniqueIDを渡していても『2.0 * ID * 3.14』が一周の位置から段々とずれてくるわけですね。この現象が大量のパーティクルで連続することでアニメーションしているように見えるということです。
無限小数の円周率によって計算誤差が発生していると覚えてもいいかもしれません。