前書き
この記事は、2023のUnityアドカレの12/8の記事です。
今年は、完走賞に挑戦してみたいと思います。Qiita君ぬい欲しい!
TL;DR;
// デリゲート型の宣言用マクロ
#define DECLEAR_DLG(retType, dlgTypeName, args) \
interface dlgTypeName \
{ \
retType Invoke args ; \
};
// デリゲートの実装用マクロ
#define IMPLEMENT_DLG(dlgTypeName, dlgName, retType, lambda) \
class __delegate__implement__##dlgName : dlgTypeName \
{ \
retType Invoke lambda \
} dlgName;
DECLEAR_DLG(bool, Predicate, (float4 item, uint index));
int FindIndex(StructuredBuffer<float4> list, uint start, uint count, Predicate predicate)
{
for(uint i = start; i < start + count; i++)
{
float4 item = list[i];
if(predicate.Invoke(item, i))
{
return (int)i;
}
}
return -1;
}
// usage
StructuredBuffer<float4> buff;
uint buffCount;
float4 PSMain() : SV_TARGET
{
IMPLEMENT_DLG(Predicate, equalZero, bool, (float4 item, uint _)
{
return all(item == 0);
});
uint index = FindIndex(buff, 0, buffCount, equalZero);
return buff[index];
}
はじめに
先日、HLSLでもinterfaceが使え、ポリモーフィズムが実現できるという記事を書きました。
ポリモーフィズムによって実現されるテクニックとして、デリゲート(コールバック)というものがあります。
C#でいうと、Array.FindIndex<T>
なんかが代表的でしょう。条件に合致するインデックスを探すというメソッドですが、「条件を判定するという部分」を引数で外部から与えるようになっています。判定する(predicate)というふるまいに対して、具体的な実装がそれぞれであるという点からポリモーフィズムと言えます。
public static int FindIndex<T>(T[] array, int startIndex, int count, Predicate<T> match)
{
... // 引数のエラーチェック
int endIndex = startIndex + count;
for (int i = startIndex; i < endIndex; i++)
{
if (match(array[i]))
return i;
}
return -1;
}
var equalZero = new Predicate<Vector4>(item =>
{
return item == Vector4.zero;
});
var index = Array.Find(array, 0, array.Length, equalZero)
これをHLSLでも実現しようという試みです。
ベタに書いてみる
デリゲートを実現するには、デリゲート型と、実装が必要です。
デリゲート型は、呼び出し規約(戻り値と引数)を宣言します。これはinterfaceで実現できます。C#では、delegate
キーワードを使って関数風に宣言できますが、HLSLにはそういったものがないので、Invoke関数を持ったclassのinterfaceということにします。
interface Predicate
{
bool Invoke(float4 item, uint index);
};
これに対応する実装はこうなります。
(HLSLは、関数内でクラスの定義が可能です)
void main()
{
class EqualZero : Predicate
{
bool Invoke(float4 item, uint index)
{
return all(item == 0);
}
} equalZero;
uint index = FindIndex(buff, 0, buffCount, equalZero);
}
マクロ化
毎度このようにべた書きしてもよいのですが、ボイラープレートコードが多いですし、何を意図してやっているのかよくわからないので、マクロを組みます。可変な部分は、デリゲート型名と、戻り値、引数の3つに、実装側では、インスタンスの変数名と、実装が必要です。
// デリゲート型の宣言用マクロ
#define DECLEAR_DLG(retType, dlgTypeName, args) \
interface dlgTypeName \
{ \
retType Invoke args ; \
};
// デリゲートの実装用マクロ
#define IMPLEMENT_DLG(dlgTypeName, dlgName, retType, lambda) \
class __delegate__implement__##dlgName : dlgTypeName \
{ \
retType Invoke lambda \
} dlgName;
宣言マクロDECLEAR_DLGを使い、デリゲート型dlgTypeNameを宣言します。具体的なデリゲートを定義実装する際には、dlgTypeName型を指定して、lambdaに実装を書きます。HLSLにラムダ式なんてものはありませんが、マクロはしょせんテキストで展開するだけなので、ラムダ式風味に書くことができます。
DECLEAR_DLG(bool, Predicate, (float4 item, uint index));
...
float4 PSMain() : SV_TARGET
{
IMPLEMENT_DLG(Predicate, equalZero, bool, (float4 item, uint _)
{
return all(item == 0);
});
uint index = FindIndex(buff, 0, buffCount, equalZero);
return buff[index];
}
Unityで使えるか?
この通り、UnityのHLSLでもinterfaceが利用可能です。(HLSLcc)
本記事でのデリゲートはマクロにより、ただのinterfaceに展開されるので、もちろん利用することが可能です!例のごとく、全部インライン展開されるので、オーバーヘッドもありません。
まとめ
HLSLでデリゲート風味のことが実現できました。本当は、C#のようにインラインでラムダ式を書くことができればよいのですが、さすがにそこまでは厳しそうです。
インターフェース、ポリモーフィズムという概念が使えることによって、これに限らず、抽象的な便利機能がいろいろ実現できそうです。