プログラミング
浮動小数点数の小数部分がシェーダーを修正するとき
When the fractional part of a float fixes your shader (crocidb.com)
要約
この記事は、音楽ビデオの背景に使用するために実装されたボロノイ図シェーダーで発生した奇妙なカクつきのデバッグ体験について詳述しています。この問題は特定のWindows PCでのみ発生し、最終的にはノイズ関数内の`fract`呼び出しにおける浮動小数点数の精度問題に起因することが判明しました。著者はシェーダーの実装、LLMを用いたデバッグプロセス、そして原因究明の過程を解説しています。
全文翻訳
先日、最近完成した音楽プロジェクトを示すために、音楽ビデオの背景として使用する小さなボロノイ図シェーダーを実装したいと思いました。
ボロノイノイズは、私が以前実装したことのないアルゴリズムの一種であり、その幾何学的でしばしば有機的な見た目に常に魅了されていました。
あまり明確な方向性を持たずに実装と実験を開始しました。かなりクールな見た目になり、その日は終わりにしようとしていましたが、翌朝、私のコンピューターの1台だけでアニメーションに奇妙なカクつきがあることに気づきました。そこで、問題の原因を突き止めるために掘り下げることにしました。
これは、シェーダーのデバッグと逆アセンブルに費やした1週間の冒険の記録です。物語にはいくつかのどんでん返しがあり、うまくいけば多くの興味深い情報も含まれているはずです。
これが最終的なシェーダーです。Shadertoyでも確認できます。まだビデオには取り掛かっていませんが、すべてのデジタルプラットフォームで音楽をチェックできます:stuffy knows。
シェーダー
このシェーダーはそれほど複雑ではありません。実際、それはいくつかの概念を組み合わせたものです。
ボロノイ図
いわゆるワーリーノイズ、またはボロノイノイズのバリエーションで、セルを互いに区別します。この場合、空間を均等なタイルに分割し、これらのタイル内にランダムな点(ボロノイ中心)を取得し、最後に各ピクセルから最も近い9つのボロノイ中心までの距離を計算します。最も近いものが、その点が属するボロノイセルを定義します。これにより、モノクロでこれが生成されます。
voronoi diagrams in black and white
The Book Of Shaders には、ボロノイノイズの非常に優れた入門があります。私がシェーダーを学び始めた頃に見た最初のリソースだったと思います。そこには他にも非常にクールなリソースがたくさんあります。
UVラッピング
これは空間の歪みです。空間を均等に分割する前に、ノイズを使用して空間を歪ませます。これをボロノイ図に適用すると、これが得られます。
distorting the space
パレットルックアップ
ボロノイセルは、基本的に中心からの距離の逆数で色付けしました。したがって、最終的に、パレットから最も適切な色(それらが望ましい順序にあると仮定して)を取得し、パレットに7色しかないため、少しシェーディングを適用します。
const vec3 palette[7] = vec3[7](
vec3(0.008, 0.451, 0.325), // #027353
vec3(0.090, 0.275, 0.090), // #174617
vec3(0.000, 0.455, 0.545), // #00748B
vec3(0.949, 0.361, 0.745), // #F25CBE
vec3(0.659, 0.580, 0.949), // #A894F2
vec3(1.000, 0.725, 0.820), // #FFB9D1
vec3(0.788, 0.949, 0.675) // #C9F2AC
);
/// ...
col = palette[i] + vec3(val - .5) * .8;
これにより、これが生成されます。
the final look for the shader
また、パレットを移動させることで、私が非常に気に入っているこのポップで動きのある効果を生み出しています。最終的なシェーダーはここで利用可能です。
問題
翌朝、最初にシェーダーを構築していたコンピューターとは別のコンピューターで作業していたため、それを微調整するために開いたときに、以前は見えなかった奇妙なカクつきに気づきました。
最初はWindowsのFirefoxで試していましたが、次にChrome、そしてEdgeで開きました。すべてのブラウザで同じ問題が表示されました。そこで、問題がどこにあるのかを見つけるために、エフェクトを削除し始めました。
UVワーピング、パレットサイクリングを削除し、セルを大きくすると、問題がより明確になりました。
比較のために、本来はどう見えるべきかを示します。
問題は、ボロノイ中心を生成するコードの部分にあるようでした。
// get distance to all points
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
vec2 o = origin + (vec2(i, j) * tile_size);
vec2 v = o * 398.0 + vec2(iTime * 1.3, iTime * 1.4);
vec2 c = o + noise2(v) / TILES;
float d = distance(uv, c);
if (d < dist) {
dist = d;
point = o;
}
}
}
より具体的には、vとcを定義する行です。ノイズ呼び出しを手続き的に構成するルールと、呼び出し自体です。どういうわけか、このノイズ呼び出し内の何かが、私の他のコンピューターでは動作したのに、このコンピューターでは異なって動作していました。
そこで、これらの異なるデバイスでテストしました。
統合Intel GPUを搭載した2台の異なるLinuxラップトップ:正常
Google Pixel 9 Proフォン:正常
RTX 2070を搭載したLinux PC:正常
RTX 4070を搭載したWindows PC:問題あり
これらの5つの異なるデバイスのうち、カクつきが発生したのは最後の1つだけでした。ほとんどすべてのデバイスで異なるブラウザも試しました。
グラフィックスプログラマーの友人に尋ねましたが、忙しすぎて手伝ってもらえませんでした。そこで、手元にあるツールでデバッグすることにしました。
ノイズ関数
シェーダープログラミングを全く知らない人のために、簡単な紹介をします。シェーダーはGPUで実行されるプログラムです。グラフィックスパイプラインのどの部分にいるかによって、いくつかの種類のシェーダーがあります。Shadertoyはフラグメントシェーダー(またはピクセルシェーダーとも呼ばれます)を使用します。これは、GPUがオブジェクトの頂点をラスタライズした直後のステージであり、このプログラムが最終的なピクセル色を生成する責任を負います。要約すると。
プログラムは基本的に、フレームごとにピクセルごとに実行されます。Shadertoyビューポートでは。
状態を保存できないため、副作用はありません。関数型プログラミングを考えてみてください。シェーダー全体が純粋関数のようなもので、いくつかの入力を受け取り、それに基づいて常に同じ出力を生成します。それがShadertoyが楽しく遊べる理由の一部です。
私のシェーダーで常に変化する2つのパラメータは次のとおりです。
1. 現在のピクセルの座標。
2. 時間変数。
前者は2次元ベクトルとして渡され、その成分は0.0fから1.0fの範囲です。後者は浮動小数点数であり、フレームごとにのみ変化します。
ランダムな値を取得するには、ハッシュ関数に依存し、その後、それを平滑化するために手続き的なノイズを作成する必要があります。
このシェーダーで使用されているノイズ関数は、Shadertoyの作成者であり、私が知る最も影響力のあるグラフィックスプログラマーの一人であるInigo Quilezによる、手続き型ノイズに関するこの記事から取られました。記事とはわずかに異なりますが、2019年以来、私が書いたほぼすべてのシェーダーで同じコードを使用しています。
このシェーダーで使用されている完全なノイズコードは次のとおりです。実際に理解する必要はありませんが、基本的に異なる値のハッシュを取得し、それらを補間することで、効果的に出力を平滑化します。よりよく理解したい場合は、Inigo Quilezの記事を確認してください。
float hash1(float n) {
return fract(n * 17.0 * fract(n * 0.3183099));
}
float noisev(in vec3 x) {
vec3 p = floor(x);
vec3 w = fract(x);
vec3 u = w * w * w * (w * (w * 6.0 - 15.0) + 10.0);
float n = p.x + 317.0 * p.y + 157.0 * p.z;
float a = hash1(n + 0.0);
float b = hash1(n + 1.0);
float c = hash1(n + 317.0);
float d = hash1(n + 318.0);
float e = hash1(n + 157.0);
float f = hash1(n + 158.0);
float g = hash1(n + 474.0);
float h = hash1(n + 475.0);
float k0 = a;
float k1 = b - a;
float k2 = c - a;
float k3 = e - a;
float k4 = a - b - c + d;
float k5 = a - c - e + g;
float k6 = a - b - e + f;
float k7 = -a + b + c - d + e - f - g + h;
return (k0 + k1 * u.x + k2 * u.y + k3 * u.z + k4 * u.x * u.y + k5 * u.y * u.z + k6 * u.z * u.x + k7 * u.x * u.y * u.z);
}
問題を見つけるための最初の試み
友人がデバッグの方向性を示してくれなかったので、LLMを起動し、問題と完全なコードを説明しました。最初はハッシュ関数を分析して連続性の問題を見つけようとしましたが、これは何千ものShadertoyシェーダーで使用されているコードなので、良い考えではないと思いました。そこで、それをそらしました。
次に、ノイズ関数の冒頭にある`fract`の呼び出しに問題がある可能性があると判断しました。どうやら、Nvidia 40xxドライバーでの問題についてオンラインで不満を言っている人々を見つけたようです。`fract`は浮動小数点数の小数部分を取得します。
思考セクションを読み進めると、LLM(Kimi K3)は、コードが実行されてしばらくしてからカクつきが始まったと仮定していることに気づきました。これは確かに浮動小数点数の精度を低下させ、目に見えるカクつきを引き起こす可能性があります。これは非常に一般的なことですが、このケースでは、コードが実行されてから...