WebGL
Emscripten
WebAssembly

EmscriptenでWebGL 2.0+おまけ

WebGLとは

まず、WebGLはブラウザ上でOpenGL ESをレンダリングするAPIでPCのみならず、スマートフォンブラウザで動作します。
Unityなどのゲームエンジンがブラウザ上で動作するのはこのWebGL APIのおかげです。

WebGLは今年2月にバージョンアップが行われ、WebGL 1.0はOpenGL ES 2.0相当だったのですが、
WebGL 2.0ではOpenGL ES 3.0相当となります。
OpenGL ES 3.0はOpenGL ES 2.0との互換性を保ちつつ、新規APIが実装され3DCGの表現力やパフォーマンスの向上が期待されます。
スマートフォンにおいては2017年12月現在Androidの約半数がOpenGL ES 3.0の描画に対応しており、今はOpenGL ES 2.0からOpenGL ES 3.0への過渡期ともいえます。

WebGL 2.0ではOpenGL ES 3.0から一部機能が削減されたものが実装されています。

EmscriptenでWebGL 2.0を使う

実はこのWebGL 2.0をEmscriptenで使用するのは実は拍子抜けするほど簡単でリンク時に-s USE_WEBGL2=1を指定するだけです。

EmscriptenではOpenGLのモードが3つ用意されています。

ひとつはWebGL Friendlyモードです。デフォルトではこのモードが使用されます。これはWebAssemblyからWebGL APIを直接使用するモードです。

もうひとつはOpenGL ES エミュレーションモードです。
前述の通り、WebGLはOpenGL ESから一部機能を削減して実装されています。
その一つがメモリからの直接レンダリングです。WebGLではVertex Buffer Objectといって、OpenGL用のバッファを作成してからレンダリングするモードしか用意されていないため、純然たるOpenGL ES 2.0/3.0実装であったとしても動作しないことがあります。
メモリからの直接のレンダリングのようにWebGL APIには存在しないOpenGL ES APIを実行可能にするのがこのエミュレーションモードです。
OpenGL ES 2.0の機能をエミュレートするには-s FULL_ES2=1フラグをリンク時に指定し、OpenGL ES 3.0の機能をエミュレートするには-s FULL_ES3=1を指定します。
このオプションはどちらか一方、または両方同時に使用できます。

最後はレガシーなデスクトップ用のOpenGL APIのエミュレーションモードです。
これは最近は使われなくなった古いOpenGL APIを使用する場合にはこのモードを使用します。
このモードを使用する場合は-s LEGACY_GL_EMULATION=1オプションを指定します。

サンプル実装

さて、WebGL 2.0を早速試してみたいと思います。
サンプルプログラムとしてこちらのChapter 11のMRTを試してみます。
フレームワークはWebGL用にEmscriptenでビルド可能なglfw3を使用して書き直します。
MRTは1度に複数のバッファに書き込む機能でWebGL 1.0では拡張機能での実装でしたが、WebGL 2.0で正式APIとして実装されたものです。
WebGLではGLSLと呼ばれる言語でバッファへの書き出しを行うのですがこの言語もWebGLのバージョンアップと同時に1.0から3.0にバージョンアップします。

以下は従来のWebGL 1.0のシェーダです。
gl_FragColorに灰色を出力します。

WebGL
precision mediump float;

void main()
  // fourth buffer will contain gray color 
  gl_FragColor = vec4 ( 0.5, 0.5, 0.5, 1 );
}

一方で、以下がWebGL 2.0のシェーダです。
いちどに4バッファの書き出しを行っています。
変数のレイアウトもWebGL 2.0ならではの機能です。

WebGL2
#version 300 es
precision mediump float;
layout(location = 0) out vec4 fragData0;   
layout(location = 1) out vec4 fragData1;   
layout(location = 2) out vec4 fragData2;   
layout(location = 3) out vec4 fragData3;   

void main()
  // first buffer will contain red color
  fragData0 = vec4 ( 1, 0, 0, 1 );

  // second buffer will contain green color
  fragData1 = vec4 ( 0, 1, 0, 1 );

  // third buffer will contain blue color
  fragData2 = vec4 ( 0, 0, 1, 1 );

  // fourth buffer will contain gray color 
  fragData3 = vec4 ( 0.5, 0.5, 0.5, 1 );
}

実行結果です。4バッファ書き出した結果を画面出力しています。
result1.png

Spectorで見てみました。1ドローで4バッファ書き出しているのがわかります。
result3

もちろんWebGL 2.0です。
result4.png

デモソースコードです。

おまけ

さて、おまけでちょっと気になって調べたことを書きます。
WebGLでは大量の行列の掛け算を行います。
そのため、この部分だけでもWebAssemblyの力で高速化できないかと試してみました。
行列の計算1回ごとにWASMの関数を呼び出しているのですがオーバーヘッドも気になります。どうでしょうか?

WebGLで使う前提なので配列はcolumn-majorです。

JavaScript実装
        const MultiplyMatrixJS = function(a, b, dst) {
            for(let x = 0; x < 4; x++) {
                for(let y = 0; y < 4; y++) {
                    let i = x * 4;
                    dst[i + y] =
                        a[0 + y] * b[i + 0] 
                        + a[4 + y] * b[i + 1] 
                        + a[8 + y] * b[i + 2] 
                        + a[12 + y] * b[i + 3];
                }
            }
        };
WebAssembly実装
    void MultiplyMatrix(float *a, float *b, float *dst)
    {
        for(auto x = 0; x < 4; x++) {
            for(auto y = 0; y < 4; y++) {
                auto i = x * 4;
                dst[i + y] =
                      a[0 + y] * b[i + 0] 
                    + a[4 + y] * b[i + 1] 
                    + a[8 + y] * b[i + 2] 
                    + a[12 + y] * b[i + 3];
            }
        }
    }

それぞれ1000000回ぐらい演算した結果がこちらです。

result2.png

実行してみた結果、私の環境だとWASMのほうが処理速度が速い結果となりました。
この結果だけを見るとちょっとした演算でもWASMを使いたくなりますね。
他の環境だとJavaScriptのほうが速いこともありました。皆さんはどうでしたか?

デモソースコードです。

参考

OpenGL support in Emscripten
Qiita | WebGL2.0の概要
wgld.org | MRT (Multiple Render Targets)