-->

【Unity Shader 基礎】if文の代わりとなるShader関数

【Unity Shader 基礎】if文の代わりとなるShader関数
関連外部リンク
Shaderは1ピクセルずつ処理を実行するので、同じ関数を1フレームごとに何千何万と繰り返し通過します。
このため処理に時間がかかるif文を使ったコードが非推奨となっていて、特殊な代替え処理をよく使います。
ShaderのHLSL言語がC言語の派生と言われながらも、初見では全くそうは見えないのもここに一因があります。

step関数

C#ユーザーが見慣れないHLSLの関数は、基本的にはC#の記述で置き換えることができます。
例えば
 step(a, b) 
という関数は、aがb以下なら1を返し、それ以外は0を返します。
Copy Code

float a = 0.2f;
float b = 0.4f;

//resultには1が代入される
float result = step(a, b);
このstep関数をC#に置き換えるとこうなります。
Copy Code

private float Step(float a, float b)
{
    if (a <= b)
    {
        return 1;
    }
    else
    {
        return 0;
    }
}
step関数はC#に置き換えることが出来るので、先にC#で実装を考えてからHLSLに置き換える実装も可能です。

置き換える際にちょっと厄介な場面もあります。たとえば上のコードの「<=」という条件式が「<」であった場合。
Copy Code

private float Step(float a, float b)
{
    if (a < b)
    {
        return 1;
    }
    else
    {
        return 0;
    }
}
step(a, b)というコードでは、aとbが同じ値だと1を返してしまいます。
この場合周りくどいですが、
 1 - step(b, a) 
というコードにすると同じ意味になります。
Copy Code

//aとbが同じ値なら0を、aがbより小さいなら0を、aがbより大きいなら1を返す
1 - step(b, a);

min関数、max関数、lerp関数

 min 
関数は、2つの渡された引数のうち小さい方を返します。
 max 
関数は、2つの渡された引数のうち大きい方を返します。両方ともシンプルな関数です。
Copy Code

float a = 0.2f;
float b = 0.4f;

//resultMinには0.2が代入される
float resultMin = min(a, b);
//resultMaxには0.4が代入される
float resultMax = max(a, b);

 lerp 
関数は、第1引数を開始値、第2引数を終了値として、第3引数に渡される0~1の値を基準に、第1第2の中間値を返します。
言葉で説明すると複雑に見えますが、コードで示すと要はこういうことです。
Copy Code

//resultには5が代入される
float result = lerp(5, 10, 0);
//resultには7.5が代入される
float result = lerp(5, 10, 0.5);
//resultには10が代入される
float result = lerp(5, 10, 1);
第1引数 < 第2引数の関係性である必要はありません。
Copy Code

//resultには-50が代入される
float result = lerp(0, -100, 0.5);

step、min、maxでif文の代わりを作る

 step 
と、
 min 
,
 max 
,
 lerp 
などの関数を併用すると、C#の複雑な条件式もある程度置き換えて実装できます。
これはどんな数値であろうと0を掛けると0になってしまう常識と、どんな数値に1を掛けたり割ったりしても値が変化しない常識を利用します。

例えば下のようなC#の関数があったとします。
Copy Code

//divisionが0以下ならoriginalの値をそのまま返す。
//それ以外はorginalの値にdivisionを割った値を返し、
//またdivisionの最大値は1000とする。
private float Calc(float original, float division)
{
    if(division <= 0)
    {
        return original
    }

    division = division > 1000? 1000 : division;
    return original / division;
}
これをShaderのHLSL言語に訳すとこうなります。
Copy Code

float calc(float original, float division)
{
    //divisionが0以下なら0を返し、それ以外は1を返す
    float avoidZero = 1 - step(division, 0);
    //もしavoidZeroが0なら1を返し、1ならdivisionの値をそのまま返す
    float fixDivision = max(1 - avoidZero, division * avoidZero);
    //fixDivisionと1000を比較し、小さい方を返す
    fixDivision = min(fixDivision, 1000);

    //originalの値にfixDivisionを割って返す
    return original / fixDivision;
}

あるいはmaxの代わりにlerpを使うとこういう書き方もできます。 こちらの方が直観的です。
Copy Code

float calc(float original, float division)
{
    //divisionが0以下なら0を返し、それ以外は1を返す
    float avoidZero = 1.0 - step(division, 0);
    //もしavoidZeroが0なら1を返し、1ならdivisionの値をそのまま返す
    float fixDivision = lerp(1, division, avoidZero);
    // fixDivisionと1000を比較し、小さい方を返す
    fixDivision = min(fixDivision, 1000.0);

    // originalの値にfixDivisionを割って返す
    return original / fixDivision;
}

if文を使っても良いケース

たとえばピクセルシェーダー内で必ず同じ遷移をたどるケースであれば、if文を使っても問題ないです。
Copy Code

float _isOutGame; //シェーダーの外側から渡される値とする

float calc(float original, float division)
{
	//_isOutGameの値が1なら即 0 を返す
	if(_isOutGame == 1)
	{
		return 0;
	}

	//divisionが0以下なら0を返し、それ以外は1を返す
	float avoidZero = 1.0 - step(division, 0);
	//もしavoidZeroが0なら1を返し、1ならdivisionの値をそのまま返す
	float fixDivision = lerp(1, division, avoidZero);
	// fixDivisionと1000を比較し、小さい方を返す
	fixDivision = min(fixDivision, 1000.0);

	// originalの値にfixDivisionを割って返す
	return original / fixDivision;
}
このケースでは、呼ばれるピクセルシェーダーごとに遷移が分岐することはありません。
例えば、ある座標のピクセルの計算では
 return 0; 
を返し、その隣の座標では
 return original / fixDivision; 
を返す、といった事はないです。
同じ1フレーム内で、外から渡される
 _isOutGame 
の値が変化することはないからです。

こうした分岐を
 一様分岐 
と言い、シェーダーの処理に負荷を与えることはないです。
0
0