Emscripten で OpenGL コードの Web 移植を試す

# 概要
- [OpenSiv3D](https://github.com/Siv3D/OpenSiv3D) を Web で動かしたい 
 - 本体の移植は行うのは~~間に合わなかった~~規模が大きすぎるためまずは小さい OpenGL のプログラムで実験をした
- 実用性を考えるなら手書きで OpenGL ES 2.0 に移植するのが一番良さそう

# OpenGL 4 の Web 移植
現行の OpenSiv3D Linux 版では **OpenGL 4.1** が使われています。
OpenGL 4.1 から WebGL に直接変換できるのが理想ですが、[OpenGL support in Emscripten](https://emscripten.org/docs/porting/multimedia_and_graphics/OpenGL-support.html) には Emscripten の提供する OpenGL サポートについて以下のようにあります。

> - WebGL-friendly **subset of OpenGL ES 2.0/3.0** (default) — supports the set of OpenGL ES 2.0/3.0 commands that map directly to WebGL 1/2.
> - OpenGL ES 2.0/3.0 emulation — support for some emulated OpenGL ES 2.0/3.0 features that are not present in WebGL.
> - Emulation of older Desktop OpenGL API features — support for a number of legacy **GL 1.x** features and commands.

Emscripten が WebGL に変換できるグラフィックス命令は **OpenGL ES 2.0/3.0** か **OpenGL 1.x** に限られているようです。
つまり OpenGL のコードをまず OpenGL ES に移植できないとどうにもならなさそうですが、これについて次のような補足がありました。

> You can consider building the codebase against the **Regal Desktop OpenGL emulation library**, which aims to support Desktop OpenGL features on top of **OpenGL ES 2.0**.

どうやら **Regal** というライブラリを使うことで OpenGL のエミュレーションが行えるみたいです。

## Regal
https://github.com/p3/regal

Regal は OpenGL 互換のライブラリで、**OpenGL を呼び出すコードを Regal にリンクすることで自動的に OpenGL ES 2.0 を使ったプログラムを出力**することを目標としているようです。
さらに Regal は特定の環境に依存せず、提供する API は **OpenGL 4.4** まで対応しているそうです。
素晴らしいですね。

しかし README を見ると現在開発中と書いてあるにもかかわらず近年更新されている様子はありませんでした。
調べてみると Regal は主に NVIDIA や Google の人らによって開発されていたものの、メインの開発者が NVIDIA を退社した **2014 年から開発がほぼ止まっている**ようです。

不安になってきましたが、実用上問題ないことを祈りながら使ってみます。

Regal の使い方自体はとても簡単で OpenGL を使っているコードの `GL/gl.h` のインクルードを `GL/Regal.h` に置き換えて初期化後に次の関数を呼び出すだけです。

```cpp

RegalMakeCurrent((RegalSystemContext)1);
```

Emscripten でコンパイルする場合はこれに加えて [Implementing an asynchronous main loop in C/C++](https://emscripten.org/docs/porting/emscripten-runtime-environment.html#implementing-an-asynchronous-main-loop-in-c-c) にあるようにメインループを `emscripten_set_main_loop()` で置き換える必要があります。

これでとりあえず OpenGL のプログラムを OpenGL ES に移植できたと思うことにします。

## GLSL のバージョン変換

グラフィックス命令を OpenGL ES に置き換えたらシェーダもそれに合わせて置き換える必要があります。

[Vulkan SDK](https://www.lunarg.com/vulkan-sdk/) には GLSL から SPIR-V へと変換するコンパイラ **glslangValidator** と SPIR-V の逆コンパイラ **spirv-cross** が提供されているため、これらを通すことで対応する命令は自動的に変換することができます。

```bash:OpenGLからOpenGLES2.0のシェーダに変換する例

glslangValidator -G --aml -S vert ./main.vs -o main.vs.spv
glslangValidator -G --aml -S frag ./main.fs -o main.fs.spv

spirv-cross --version 200 --es ./main.vs.spv --output main_es200.vs
spirv-cross --version 200 --es ./main.fs.spv --output main_es200.fs
```

## 動かしてみた例
[antons_opengl_tutorials_book](https://github.com/capnramses/antons_opengl_tutorials_book) の 00_hello_triangle サンプルを上記の方法で Emscripten でコンパイルしてみました。（こちらは OpenGL 3.0 で動くコードですが他に手軽なサンプルが見つからなかったので許してください…）

オリジナルとの差分は少ないですがコード自体は長いのでリンクだけ置いておきます。
https://github.com/agehama/00_hello_triangle_emscripten

```cmake:CMakeLists.txt
cmake_minimum_required(VERSION 2.8)

set(CMAKE_BUILD_TYPE Release)

add_executable(main main.c)
set_target_properties(main PROPERTIES LINK_FLAGS "${LINK_FLAGS} -s USE_GLFW=3 -s USE_REGAL=1 -s FULL_ES2=1")

set(CMAKE_EXECUTABLE_SUFFIX ".html")
```

この `CMakeLists.txt` に対して `emcmake cmake` と `make` を実行すれば `main.html` `main.js` `main.wasm` の三つが生成されます。

### 実行結果
こちらは意外性の無い実行結果ですが、OpenGL ES 3.0 の機能である `VertexArrayObject` が OpenGL ES 2.0 を通したコンパイルの結果として動いており Regal が機能していることがほんのりと感じられます。
![ss.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100367/95033e0a-fd8d-fb7e-e6ed-130a6a837939.png)
ちなみに他のコードを試したところ`glDrawElementsBaseVertex()`は動きませんでした。

# 参考：D3wasm
これだけだと使えるかどうかよくわからないので Regal を使った移植を真面目に試みた事例の紹介をします。

[D3WASM: A PORT OF ID TECH 4 / DOOM 3 ENGINE TO WEBASSEMBLY](http://www.continuation-labs.com/projects/d3wasm/)
こちらは **Doom 3 を Emscripten で WebAssembly と WebGL に移植した**プロジェクトの記事だそうです。すごい。

レンダラの実装の部分については大まかに次のように書いてありました。

> - 最初に検証として Regal を使った移植を試みた
 - まず GL 1.x の固定機能パイプラインによるレンダラを Regal 上で動かした
 - これは Regal 自体に手を加えることから始まり、かなり低レイヤまで修正した結果低速だが一応動いた
- グラフィックスを改善するために固定機能パイプラインのパスを ES 2.0 で置き換えた
 - 結果として ES 2.0 上で動く Regal と GLSL を使った ES 2.0 のレンダリングパスが混在する状況になった
- すべての固定機能パイプラインコードを ES 2.0 に書き換え Regal は取り除いた
- さらに WebGL では直接扱えない命令も使っていたのでこれを取り除いた

結局パフォーマンスを出すには WebGL の命令を直接呼び出せるように気を付ける必要があり Regal は検証用程度と考えた方が良さそうです。
しかも [Gabriel Cuvillier 氏が実際に加えた修正](https://github.com/emscripten-ports/regal/commits/master)の量を見ると Regal を使うこと自体がかなりの挑戦になる可能性がありますね…

# まとめ
- OpenGL をちゃんと Web で動かしたいなら OpenGL ES で書きなおすのが現実的そう
 - Regal を使うにしても Regal 自体に手を加える覚悟が必要
- 根本的には WebGL で扱えるグラフィックス命令の制限の問題なので、待ってればそのうち全部 WebGPU で解決しそう（適当）
 - 今は移植を頑張るより Vulkan でも触ってた方が良いのでは？
- [D3wasm の記事](http://www.continuation-labs.com/projects/d3wasm/) は **2019 年に真面目に Web 移植をやろうとするとこうなる**という知見の塊だった
- ~~Write once, run anywhere~~


# 関連記事
- [OpenSiv3DのWebブラウザ版をEmscriptenで作ろうとした話](https://qiita.com/RYOSKATE/items/8644e711bf331d978551)