前書き
この記事は、2023のUnityアドカレの12/4の記事です。
今年は、完走賞に挑戦してみたいと思います。Qiita君ぬい欲しい!
追記
この記事はUnityのシェーダを前提として書いておりました。DXCでは、HLSLのinterfaceは利用できず、DirectX12ではLinkage機能もなくなっています。将来的に使い続けられる手法ではないかもしれないということを念頭にご覧ください。
@djann9071さんご指摘ありがとうございました。
TL;DR;
HLSLでも、interfaceが使えるので、使おう
はじめに
HLSLを書いたことがある方を対象としていますが、これからShaderに挑戦したいという方にも役立つと思います。
HLSLは、Shaderを記述する言語です。UnityのShaderファイルであるShaderLabも、プログラム部分はHLSLで記述します。
HLSLは、C言語からポインタなどを排除したサブセットみたいな言語だと思っていないでしょうか?実際に、今まで他人が書いたHLSLを読んでいても、基本的にそう感じています。
しかし、HLSLはCでよりも、むしろC++のサブセットと言えます。C++と言えば、C言語に「オブジェクト指向」に便利な機能が追加されています。故に、HLSLでも「オブジェクト指向」っぽいことができるというわけです。
オブジェクト指向とは?
これは、人によって見解が分かれるかもしれません。TechReachさんの記事によれば、
- 継承
- ポリモーフィズム
- カプセル化
この3つが三大要件らしいです。まぁしかしオブジェクト指向が何かはこの際重要ではありません。
(タイトルはややふかしています、スミマセンw)
ここでは、HLSLでも「継承」と「ポリモーフィズム」が実現できますよ!ということがお伝えしたいのです。
継承とポリモーフィズとは
継承は、ある派生型が、あるベース型を継承すると、ベース型で実装されているメンバが派生型からも利用できるようになります。
class Material
{
float GeRoughness() { return _Runghness; }
};
class YellowMaterial : Material { }
YellowMaterial mat;
float roughness = mat.GetRoughness; // YellowMaterialでは継承して使える!
これにより、派生型は、少なくともベース型と同じだけの機能(ふるまい)を持っていることが保証できます。なので、同様に呼び出すことができます。
float3 CalcDiffuseColor(Material mat);
YellowMaterial yellowMat;
float diffuse = CalcDiffuseColor(yellowMat); // 型が違うはずなのに合法
ポリモーフィズムは、実際の型によって機能の中身が異なることができます。しかし、呼び出す側はそれを気にせず、同じように呼び出せるということです。
interface IMaterial
{
float3 GetAlbedoColor();
float GetRoughness();
float GetMatalness();
};
class YellowMaterial : IMaterial
{
float3 GetAlbedoColor() { return YELLOW; }
float GetRoughness() { return 0.5; }
float GetMatalness() { return 0; }
};
class BlueMaterial : IMaterial
{
float3 GetAlbedoColor() { return BLUE; }
float3 GetRoughness() { return 0.5; }
float GetMatalness() { return 0; }
};
float3 CalcDiffuseColor(IMaterial mat);
// YellowMaterialもBlueMaterialも異なるGetAlbedoColorを持つのに、同じように使える
YellowMaterial yellowMat;
BlueMaterial blueMat;
float diffuse = CalcDiffuseColor(yellowMat);
float diffuse = CalcDiffuseColor(blueMat);
もしポリモーフィズムが不可能な場合、都度分岐しなければなりません。
YellowMaterial yellowMaterial;
BlueMaterial blueMaterial;
float3 albedo;
if(yellowMaterial.enabled)
{
albedo = yellowMaterial.GetAlbedo();
}
else if(blueMaterial.enabled)
{
albedo = blueMaterial.GetAlbedo();
}
... // いろいろな処理
float roughness;
if(yellowMaterial.enabled)
{
albedo = yellowMaterial.GetRoughness();
}
else if(blueMaterial.enabled)
{
albedo = blueMaterial.GetRoughness();
}
... // いろいろな処理
float metalness;
... // metalnessも同様に
ポリモーフィズムのおかげで、一番最初の、具体的にどの型のインスタンスを渡すかで1度分岐するだけで済みます。
実は、HLSLでも継承やinterfaceが使える
実は上記のコードは、合法なHLSLだったのです!!!!
(C#やC++でのイメージコードではありません)
上記の例では、Materialの切り替えを行いました。一般的にMaterialは値のセットで、アルゴリズム(処理)を持つことはあまりないと思いますが、この機能では、フィールドではなく関数をMaterialに含めることができるのです。例えば、古典的なハーフランバートMaterial、PBRMaterial、セルルックMaterial…といった具合にです。
Microsoftの狙いとしては、Linkage機能を使うためのもののようです。Linkageは、CPU側で、インスタンスを選択し、Shaderに渡してあげるという機能です。これにより、Shaderの分岐コンパイルを抑えられるという旨味があります。
// Shaderリフレクションで、interfaceのスロット情報を取得
D3DReflect(psBuff, psSize, IID_ID3D11ShaderReflection, &reflector));
int interfaceCount = reflector->GetNumInterfaceSlots();
ID3D11ShaderReflectionVariable* variable = reflector->GetVariableByName("_Material");
int materialInterfaceOffset = variable->GetInterfaceSlot(0);
// "_YellowMaterial"インスタンス参照を取得
dev->CreateClassLinkage(&linkage);
dev->CreatePixelShader(psBuff, psSize, linkage, &pixelShader));
linkage->GetClassInstance("_YellowMaterial", 0, &instance);
linkageArray[index] = instance;
// インスタンス参照を渡してShader GO GO GO!!!
context->PSSetShader(pixelShader, linkageArray, g_iNumPSInterfaces );
Unityで使えるか?
UnityのShaderLabでも、もちろん利用することが可能です!
しかし、Linkage機能はShaderだけで完結しないので、Shader内で、性的または同的に採用するインスタンスを分岐してやる必要はあります。
オーバーヘッドについて
ありません。
Shaderのコンパイルでは、すべての関数、クラスがinline展開されます。なので、コンパイルされるときには、具体的にどの型なのかは解決されています。故に、見かけ上virtualコールですが、ランタイムではVirtualはおろか、関数呼び出しですらなくなります。
(Linkageを使っている場合はちょっと話が複雑になるので割愛)
さらなるHLSLの進化
Unityでは使えませんが、最新のHLSL2021では、更なる機能が追加されています。
- 型テンプレート
- 演算子(
+
-
*
/
など) - ビットフィールド
まとめ
言いたいことは TL;DR; のただ一点だけです。
HLSLでも、interfaceが使えるので、使おう!
ついでに言うと、classやstructのメンバ関数ももっと使っていくといいんじゃないかと思います。
Shaderプログラムが小さかった、昔ながらの名残なのか、手続的に記述されているShaderをよく見ます。というか、私は、手続的記述ではない他人の書いたShaderを見たことがありません。
ComputeShaderも当たり前に使われる時代ですし、GraphicsShaderもどんどんと規模が拡大して来ています。いかにも古のC言語…って書き方から、オブジェクト指向や関数型のいいとことも取り入れて、Shaderでも可読性、メンテナンス性を考えていけるとよいんじゃないかと思います。