WebGLのWEBGL_debug_shaders拡張は、プラットフォームごとの変換されたシェーダーソースコードを取得するAPIを提供する拡張機能です。WebGLの実行や表現の観点で機能が増えるわけではありませんが、変換されたシェーダーソースコードを確認することはデバッグやパフォーマンスの観点で役に立つことがあります。さらに、一部の環境では変換されたHLSLのコードを確認できます

WebGLの実行環境

WebGLコンテンツの開発者であれば、WebGLがどのようなバックグラウンドで動作しているかを知っておいて損はないでしょう。そもそも WebGL 1.0 は、OpenGL ES 2.0 をベースとしてブラウザ用に拡張されたAPIです。グラフィックスAPIやシェーダーについても、Open GL ES 2.0 の文法を踏襲しています。しかし、WebGLを実行可能なすべての環境でOpenGL ESが動作するわけではありません。そこで、ブラウザはそれぞれの実行環境で適した3DグラフィックスAPIのバックグラウンドを選択して実行しています。たとえば、Windows環境ではDirect3D、macOSやLinux環境ではOpenGL、モバイル環境ではOpenGL ES…といった具合です。

OpenGL ESはOpenGLの拡張なので、OpenGL ES向けのAPIをOpenGL環境で動かすことはそこまで難しくありません。しかし、OpenGL ES向けのAPIをDirect3Dで動かすには大きな変換が必要となります。シェーダーも、WebGLで使用されるGLSL(OpenGL Shading Language / ジーエルエスエル)から、Direct3DのHLSL(High Level Shading Language / エイチエルエスエル)に変換する必要があります。そのため、ChromeやFirefox、EdgeなどのWindows環境のデスクトップブラウザでは、ANGLE(Almost Native Graphics Layer Engine / アングル)というオープンソースのライブラリのレイヤー上でWebGLが動作しています。

このように、WebGLでは、ユーザーが記述したGLSLシェーダーソースコードを必ずしもそのまま使用するわけではありません。WEBGL_debug_shaders拡張を使えば、変換されたシェーダーソースコードを確認できます

サンプルの紹介

WEBGL_debug_shaders拡張を使って、実行している環境向けに変換済みのシェーダーコードを表示するサンプルを紹介します。

頂点シェーダーとフラグメントシェーダーそれぞれについて、左側のテキストボックスに入力されたGLSLコードをコンパイルし、WEBGL_debug_shaders拡張で変換されたコードを右側のテキストボックスに表示します。左側のテキストボックスを自由に編集してみてください。頂点シェーダーとフラグメントシェーダーはリンクしていないので、入出力(varying)を合わせなくても大丈夫です。

2019年4月現在の、主要な環境でのWEBGL_debug_shaders拡張の挙動を下記の表にまとめました。

OS ブラウザ 得られるソースコード
Windows Chrome ANGLE変換後のGLSL/HLSL
Windows Firefox ANGLE変換後のGLSL
Windows Edge 非対応
macOS Chrome ANGLE変換後のGLSL
macOS Firefox ANGLE変換後のGLSL
macOS Safari 非対応
Android Chrome ANGLE変換後のGLSL
iOS Safari 非対応

特筆すべきはWindowsのChromeで、変換されたHLSLコードを取得できます。WindowsのFirefoxでもANGLEでHLSLに変換されて動作しているはずですが、セキュリティのためかWEBGL_debug_shaders拡張ではHLSLコードまでは取得できないようです。

また、macOSのChromeやFirefoxでは、ANGLEレイヤー上での動作はしないものの、ANGLEのシェーダー変換機能を使って変換されたGLSLシェーダーコードが使用されます。ANGLEのシェーダー変換機能はGLSLからHLSLへの変換だけでなく、GLSLシェーダーの検証や、ディスプレイドライバーのバグに対するワークアラウンドも提供されているためです。まとめると、ANGLEを使用しているブラウザでは、下記の流れでシェーダーコードの変換が行われています。

WindowsのChrome、Firefox

[ユーザーが記述したGLSL] -> [ANGLEが変換したGLSL] -> [ANGLEが変換したHLSL] -> [グラフィックスAPI(Direct3D)]

macOSのChrome、Firefox

[ユーザーが記述したGLSL] -> [ANGLEが変換したGLSL] -> [グラフィックスAPI(OpenGL)]

WindowsのChromeでは、WEBGL_debug_shaders拡張で取得した変換済みのシェーダーコードのうち、// GLSL BEGIN// GLSL ENDで囲まれたコードが「ANGLEが変換したGLSL」を、// INITIAL HLSL BEGIN// INITIAL HLSL ENDで囲まれたコードが「ANGLEが変換したHLSL」を示しています。

参考に、上記サンプルの筆者のWindows Chrome環境で変換されたシェーダーコードを載せておきます。

// VERTEX SHADER BEGIN

// GLSL BEGIN

attribute highp vec3 webgl_74509a83309904df;
attribute highp vec4 webgl_19dff938713edbff;
varying highp vec4 webgl_68d001bdde62634f;
uniform highp float webgl_ad225f156aaf4f94;
uniform highp mat4 webgl_f4376ea35a7e1f46;
uniform highp mat4 webgl_11d5c59b099a10a2;
void main(){
(gl_Position = vec4(0.0, 0.0, 0.0, 0.0));
(webgl_68d001bdde62634f = vec4(0.0, 0.0, 0.0, 0.0));
highp float webgl_df42f98937e1a6c8 = 0.0;
(webgl_df42f98937e1a6c8 = (9.0 * cos(webgl_ad225f156aaf4f94)));
highp float webgl_ee421cea2462bca6 = (((webgl_ad225f156aaf4f94 < 0.0)) ? (1.0) : (2.0));
for (highp int webgl_6fdd29f02130ae3a = 0; (webgl_6fdd29f02130ae3a < 300); (webgl_6fdd29f02130ae3a++))
{
(webgl_ee421cea2462bca6 *= webgl_ee421cea2462bca6);
}
(webgl_68d001bdde62634f = (webgl_19dff938713edbff * webgl_ee421cea2462bca6));
highp mat4 webgl_26914b3f1ec4707e = (webgl_11d5c59b099a10a2 * webgl_f4376ea35a7e1f46);
(gl_Position = (webgl_26914b3f1ec4707e * vec4(webgl_74509a83309904df, 1.0)));
}


// GLSL END


// INITIAL HLSL BEGIN

float4 vec4_ctor(float3 x0, float x1)
{
    return float4(x0, x1);
}
// Uniforms

uniform float _webgl_ad225f156aaf4f94 : register(c0);
uniform float4x4 _webgl_f4376ea35a7e1f46 : register(c1);
uniform float4x4 _webgl_11d5c59b099a10a2 : register(c5);
#ifdef ANGLE_ENABLE_LOOP_FLATTEN
#define LOOP [loop]
#define FLATTEN [flatten]
#else
#define LOOP
#define FLATTEN
#endif

#define ATOMIC_COUNTER_ARRAY_STRIDE 4

// Attributes
static float3 _webgl_74509a83309904df = {0, 0, 0};
static float4 _webgl_19dff938713edbff = {0, 0, 0, 0};

static float4 gl_Position = float4(0, 0, 0, 0);

// Varyings
static  float4 _webgl_68d001bdde62634f = {0, 0, 0, 0};

cbuffer DriverConstants : register(b1)
{
    float4 dx_ViewAdjust : packoffset(c1);
    float2 dx_ViewCoords : packoffset(c2);
    float2 dx_ViewScale  : packoffset(c3);
};

@@ VERTEX ATTRIBUTES @@

@@ VERTEX OUTPUT @@

VS_OUTPUT main(VS_INPUT input){
@@ MAIN PROLOGUE @@
(gl_Position = float4(0.0, 0.0, 0.0, 0.0));
(_webgl_68d001bdde62634f = float4(0.0, 0.0, 0.0, 0.0));
float _webgl_df42f98937e1a6c8 = {0.0};
(_webgl_df42f98937e1a6c8 = (9.0 * cos(_webgl_ad225f156aaf4f94)));
float s40e = {0};
if ((_webgl_ad225f156aaf4f94 < 0.0))
{
(s40e = 1.0);
}
else
{
(s40e = 2.0);
}
float _webgl_ee421cea2462bca6 = s40e;
{ for(int _webgl_6fdd29f02130ae3a = {0}; (_webgl_6fdd29f02130ae3a < 300); (_webgl_6fdd29f02130ae3a++))
{
(_webgl_ee421cea2462bca6 *= _webgl_ee421cea2462bca6);
}
}
(_webgl_68d001bdde62634f = (_webgl_19dff938713edbff * _webgl_ee421cea2462bca6));
float4x4 _webgl_26914b3f1ec4707e = transpose(mul(transpose(_webgl_11d5c59b099a10a2), transpose(_webgl_f4376ea35a7e1f46)));
(gl_Position = mul(transpose(_webgl_26914b3f1ec4707e), vec4_ctor(_webgl_74509a83309904df, 1.0)));
return generateOutput(input);
}

// INITIAL HLSL END



// VERTEX SHADER END

APIの使い方

WEBGL_debug_shaders拡張の使い方は非常に簡単です。準備として頂点シェーダーを作成し、コンパイルしておきます。サンプルでは WebGL 1.0 のコンテキストを取得していますが、WebGL 2.0 でも使用可能です。

// WebGLコンテキスト(WebGLRenderingContext)を取得
const gl = canvas.getContext('webgl');

// 頂点シェーダーオブジェクト(WebGLShader)を作成
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
// シェーダーソースコードをセット
gl.shaderSource(vertexShader, shaderSource);
// シェーダーをコンパイル
gl.compileShader(vertexShader);

まず、getExtension()メソッドでWEBGL_debug_shaders拡張を取得します。そしてgetTranslatedShaderSource()メソッドにWebGLShaderオブジェクトを渡せば変換済みのシェーダーソースコードを取得できます。

// WEBGL_debug_shaders拡張を取得
const ext = gl.getExtension('WEBGL_debug_shaders');
// WEBGL_debug_shaders拡張をサポートしている場合
if (ext) {
  // getTranslatedShaderSource()メソッドで変換済みシェーダーソースコードを取得
  console.log(ext.getTranslatedShaderSource(vertexShader));
}

WEBGL_debug_shaders拡張で得られた知見

WEBGL_debug_shaders拡張で実際に変換されたシェーダーコードから得られる知見をいくつか紹介します。みなさんも試してみて面白い結果が得られたらぜひ教えてください。

使用されない変数は削除される(GLSL)

ANGLEはシェーダーのコンパイル時にGLSLを検証し、一度も使用されない変数とその演算は削除します。

// 使用されない変数
float notUsedValue = 7.0 * sin(uniformValue);

// なし

ただし、宣言時以外で一度でも参照や代入が発生した変数に関しては、最終的に出力に関わる値の演算に使用されるかどうかにかかわらず削除されません。最終的に使用されない変数とその演算については、その後のフェーズで最適化される可能性はありますが、少なくともシェーダーの変換時には残るため、極力削除しておきましょう。

// 最終的には使用されないが、代入が発生する変数
float notUsedForOutputValue;
notUsedForOutputValue = 9.0 * cos(uniformValue);

highp float webgl_df42f98937e1a6c8 = 0.0;
(webgl_df42f98937e1a6c8 = (9.0 * cos(webgl_ad225f156aaf4f94)));

同様に、一度も使用されない関数宣言も削除されます。

/**
 * 使用されない関数
 */
float notUsedfunc(float value) {
  return value * value;
}

// なし

変数は初期化される(GLSL)

ANGLEはvaryinggl_Positionなどの出力に使用する変数や、宣言時に初期化していないローカルの変数について、すべて0で初期化する処理が挿入されます。これらの初期値についてGLSLの仕様では定義されていないため、 ウェブのセキュリティ上の観点から、明示的に初期化を挿入しているようです。

6.39 Initial values for GLSL local and global variables / WebGL Specification

float notUsedForOutputValue;

(gl_Position = vec4(0.0, 0.0, 0.0, 0.0));
(webgl_68d001bdde62634f = vec4(0.0, 0.0, 0.0, 0.0));
highp float webgl_df42f98937e1a6c8 = 0.0;

行列は転置される(HLSL)

ANGLEがGLSLをHLSLに変換する際、「行列とベクトルの掛け算」や「行列同士の掛け算」に使用されている行列は、演算前にtranspose()命令で転置する処理が挿入されます。これは、GLSLとHLSLで列と行のどちらを優先するかの表現が異なるためです。行列同士の掛け算の場合、転置した行列同士を掛け算し、さらにその結果を転置することで表現します。一見するとHLSLに変換することで転置のぶんのシェーダーの処理が増えていますが、アセンブリレベルではパフォーマンスに大きな影響はないようです。

// 行列演算
mat4 mvpMatrix = projectionMatrix * modelViewMatrix;

// HLSL
float4x4 _webgl_26914b3f1ec4707e = transpose(mul(transpose(_webgl_11d5c59b099a10a2), transpose(_webgl_f4376ea35a7e1f46)));

GLSLの行列同士の掛け算をする*演算子がHLSLではmul()命令に変わっていることにも注目しましょう。HLSLでは*演算子は成分ごとの掛け算を意味するため、一般的な行列積を求めるにはmul()命令を使用します。一方、GLSLにも成分ごとの掛け算を求めるmatrixCompMult()命令があります。

三項演算子はifに置き換えられる(HLSL)

ANGLEがGLSLをHLSLに変換する際、三項演算子condition ? expr1 : expr2は、if文に置き換えられます。

// 三項演算子
float value = uniformValue < 0.0 ? 1.0 : 2.0;

// HLSL
float s40e = {0};
if ((_webgl_ad225f156aaf4f94 < 0.0))
{
(s40e = 1.0);
}
else
{
(s40e = 2.0);
}
float _webgl_ee421cea2462bca6 = s40e;

これは、GLSLの三項演算子の挙動とHLSLの三項演算子の挙動が異なるためにANGLEがif文に変換しているとのことです。最適化のために分岐を三項演算子で表現することは(少なくともWindows上で実行する場合は)残念ながら無意味ということになります。

Direct3D 9では254回以上のforループは分割される(HLSL)

現在、WindowsでANGLEを使用する場合、基本的にDirect3D 11が選択されますが、古いディスプレイドライバーを使用している場合はまれにDirect3D 9が選択されることがあります。Direct3D 9で動作する場合、ANGLEが変換したHLSLコードではシェーダー内のforループを最大254回に制限します。255回目以降のループは分割され、新たなforループのブロックが定義されます。

// 254回以上のforループ
for(int i = 0; i < 300; i++){
  value *= value;
}

// HLSL
{int _webgl_6fdd29f02130ae3a;
bool Break_webgl_6fdd29f02130ae3a = false;
 for(_webgl_6fdd29f02130ae3a = 0; _webgl_6fdd29f02130ae3a < 254; _webgl_6fdd29f02130ae3a += 1)
{
{
(_webgl_ee421cea2462bca6 *= _webgl_ee421cea2462bca6);
}
;}
if (!Break_webgl_6fdd29f02130ae3a) {
 for(_webgl_6fdd29f02130ae3a = 254; _webgl_6fdd29f02130ae3a < 300; _webgl_6fdd29f02130ae3a += 1)
{
{
(_webgl_ee421cea2462bca6 *= _webgl_ee421cea2462bca6);
}
;}
}
}

この挙動は、WindowsでChrome起動時のオプションに--use-angle=d3d9を指定することでも確認できます。

TIPS

頂点シェーダーとフラグメントシェーダーをリンクして完全なHLSLコードを確認する

WindowsのChromeの場合、getTranslatedShaderSource()メソッドにコンパイル済みのシェーダーオブジェクト(WebGLShader)を渡すとHLSLに変換されたコードを取得できます。このとき、渡したWebGLShaderが対になるWebGLShader(頂点シェーダーであればフラグメントシェーダー、もしくはその逆)とリンクする前かリンクした後かで取得できるコードが異なります。

リンク前のWebGLShaderを渡した場合、頂点シェーダーとフラグメントシェーダーの受け渡し(varying)に関する部分は空になっており、代わりに@@ MAIN PROLOGUE @@のように、@@で囲まれたコメントが挿入された不完全なHLSLコードを取得します。リンク後のWebGLShaderであれば、// COMPILER INPUT HLSL BEGIN// COMPILER INPUT HLSL ENDで囲まれたコードが追加されます。空になっていた部分にはDirect3Dのセマンティクスが挿入されており、完全なHLSLコードを取得できます。

上記のデモではリンク前のWebGLShaderを使用しています。

シェーダーの検証を無効化して変換後のコードを読みやすくする

通常、getTranslatedShaderSource()メソッドで得られる変換後のシェーダーコードは、変数名が_webgl_で始まる一見ランダムな文字列に変換され、大変読みにくくなります。Chromeでは起動時のオプションに--use-cmd-decoder=passthroughを指定することで、ANGLEの変換時のシェーダーの検証を無効化できます。シェーダーの検証を無効化した状態であれば、変数名の置換が発生しないため、比較的読みやすいHLSLコードを取得できます。ただし、ユーザーが通常実行する環境と異なりますし、他にどんな副作用があるかわからないので参考程度に留めておきましょう。たとえば、GLSLで記述したシェーダーをUnity向けに使用するためにHLSLに自動で変換したいときなどに役に立つかもしれません。

Firefoxでもabout:configwebgl.bypass-shader-validationtrueに設定することでシェーダーの検証を無効化できますが、FirefoxではHLSLコードを取得できないのでWEBGL_debug_shaders拡張の観点ではあまり役に立つことはないでしょう。

リファレンス

WEBGL_debug_shaders拡張について、詳しくは下記のドキュメントを参照ください。