Unity

if文を使わずに値が0から1の範囲内に収まっているか調べる【シェーダ最適化】

if文を使わずに値が0から1の範囲内に収まっているか調べる【シェーダ最適化】
記事内に商品プロモーションを含む場合があります

floatの値が0から1の範囲内ならA、範囲外ならBを返す処理が欲しかった。

通常であればこのようにif文で作ることができます。

if (x >= 0.0 && x <= 1.0) {
     return A;
} else {
     return B;
}

しかし、シェーダにおいてはif文を使ってしまうとGPUの並列処理を阻害することになり、パフォーマンスが大きく落ちます。

そこでif文を使わない方法を考えることにしました。

結論だけ見たい方はこちら

方針

最初に思いついたのはこれです。

int isInRange = 1 - (int)(abs(x - 0.5) + 0.5);

abs(x – 0.5)のところで絶対値が0.5に満たない場合は、+0.5しても1にならないので、int型にキャストすることで0か1に分けることができます。

最後に書きますが0か1に分けることでlerpを使用して目的の値を取得できます。

ただ、組み込み関数って中で何やってるかわからないんですよね…Cgはドキュメントに書いてありますがGLSLやHLSLは不明だし、バージョンによって違うこともありそうです。

if (x < 0) {
     return x * -1;
} else {
     return x;
}

内部がこんな感じだったら意味ないので、組み込み関数も使わない方針にしました。

補足

Cgのドキュメントを見るとabsの内部ではmaxを使用、maxは三項演算子を使っています。
各所でシェーダの最適化資料を漁ってみたところ、absは軽量な組み込み関数であるという事が書かれていることが多かったので、absを使用するやり方でも問題ないかもしれません。

たどりついたもの

int isInRange =
     (1 + (int)(x - 1.0)) *
     (1 - (int)x);

マイナス方向とプラス方向に分けてチェックして、掛け合わせればなんとかなりました。

以下のパターンは対応できないのですが、今回は必要なかったので考えないことにしました。

  • x <= -1
  • x >= 2

教えてもらったもの

結局こういうのは強いエンジニアに聞けということで、数学が得意な人に聞いたら先ほどの対応できなかったパターンも対応できる方法を教えてもらえました。

int v = (int)((x - 0.5) * 2);
float isInRange = (float)v / (v - ε);

εはイプシロン定数(CのFLT_MIN、Unityのfloat.Epsilonなど)で限りなく小さい値です。

シェーダ言語には用意されていなかったりするので、自前で用意する必要はあります。

0.000…00001とか適当に定義すればOKです。

厳密に0 or 1になるわけではないのですが、次に書くようにlerpで使用するので誤差は気にしなくていいレベルです。

目的の値を取得

あとは線形補間(lerp)で目的の値を取得するだけ!

return lerp(B, A, isInRange);

補足

lerpも組み込み関数ですが、内部的にはこんな感じになっているはずなので問題ないと判断しました。

lerp(start, end, t) {
    return (1 - t) * start + t * end;
}

パフォーマンス計測

2019/11/27追記
Unityエディタ × RenderDocを使って、簡単に負荷検証をしてみました。

前提
  • 実行環境はWinノートPC/GeForceGTX1060/DX11
  • 1ピクセルに対してforループで1000回実行
  • trueの値もfalseの値もとり得る入力値を入れる
if文120ms
三項演算子90ms
記事内の各方法45〜46ms

記事内の方法はいずれもパフォーマンスに大差はなかったので、最初のabsを使う方法でも大丈夫そうということが分かりました。組み込み関数を使うのは問題なさそうです。

値が0〜1に収まっているか調べたいことは多くはないと思いますが、if文を使うよりは三項演算子を使った方が早いということも言えそうです。