C-WebGL: シェーダのHLSLへの変換
VulkanはSPIR-Vと呼ばれる中間言語をGPUドライバに渡して処理を行うが、他のGPU API(WebGPU、Metal、DirectX11)ではソースコードの形でGPUドライバに渡す必要がある。これ用の変換は SPIRV-Cross で行える。
(DirectXは伝統的にDXBC(DirectX11やそれ以前)やDXIL(DirectX12)と呼ばれる中間言語を使用する。しかし、Vulkanとは異なり最近のDirectXではAPIの実行環境にシェーダコンパイラが存在することが保証されているので、ソースレベルの処理で実装すれば十分と言える。)
...そもそもGoogleのOpenGL ES実装であるANGLEのMetalレンダラが既に同様の方針でSPIRV-Cross を使っていて、C-WebGLにも既に組込まれているので、それを直接呼べば目的を達成できる。
変換されたシェーダ
GLSLとHLSLは割とセマンティクスが異なり、特にHLSLはnVidiaのCgを元にして作られた歴史的事情から自然なCプログラムからは割と乖離がある。
定数バッファ
layout(set = 0, binding = 0, std140) uniform cwgl_ubo_s
{
float _Extrude;
vec4 hlslcc_mtx4x4unity_ObjectToWorld[4];
vec4 hlslcc_mtx4x4unity_MatrixVP[4];
vec4 _MainTex_ST;
} cwgl_ubo;
cbuffer cwgl_ubo_s : register(b0)
{
float cwgl_ubo_Extrude : packoffset(c0);
float4 cwgl_ubo_hlslcc_mtx4x4unity_ObjectToWorld[4] : packoffset(c1);
float4 cwgl_ubo_hlslcc_mtx4x4unity_MatrixVP[4] : packoffset(c5);
float4 cwgl_ubo_MainTex_ST : packoffset(c9);
};
HLSLでは、基本的に各レジスタはfloat4のサイズがあると仮定し、レジスタ単位で値をストアしていく。(GLSLも、標準ではfloat4単位でpackingを行う点は共通している。)
例えば、 packoffset(c5)
は constant の 5番目から値をストアすることを示す。GLSLの例では密にpackできているので、offsetは省略されている。
入出力
layout(location = 0) in vec3 in_NORMAL0;
layout(location = 1) in vec4 in_POSITION0;
layout(location = 0) out vec2 vs_TEXCOORD0;
layout(location = 2) in vec2 in_TEXCOORD0;
struct SPIRV_Cross_Input
{
float3 in_NORMAL0 : TEXCOORD0;
float4 in_POSITION0 : TEXCOORD1;
float2 in_TEXCOORD0 : TEXCOORD2;
};
GLSLでは、シェーダ入出力はグローバル変数によって行う。HLSLでは struct
を main
関数で入出力することで行い、その struct
を セマンティクス で修飾することで頂点ストリームやテクスチャのようなopaqueなuniformを表現する。
この差を埋めるために、SPIRV-Crossは自動的に main
関数をwrapするようになっている。
... この例では、GLSL側の変数が in_NORMAL0
だの in_TEXCOORD0
だの命名されているので非常にややこしい。。これはUnityが供給してくるGLSLソースがそうなっているため。。UnityはCgというかHLSLというか何というかなシェーダ言語を、各処理系向けに変換して使用している。