TL;DR
OpenSiv3Dの内部実装もEmscriptenもOpenGLもよくわかっていない人間がOpenSiv3Dのブラウザ版を作ろうとしたら、やっぱり難しかった。
はじめに
EmscriptenというC++のコードをWebブラウザで動くJavaScriptに変換してくれるコンパイラがあります。
既存のOpenSiv3DのコードをEmscriptenでコンパイルすればサクッとお手軽にWebブラウザ版が作れるのではないか、と浅はかにも思いついてしまったのがコトの始まりです。
この記事ではその進捗の軌跡と言う名の紆余曲折のようなものを紹介します。
1. Emscriptenの準備
開発環境はWindows 10です。
まずはEmscriptenをインストールしました。
これは別記事にしてあります。
Emscriptenの導入メモ(Windows)
2. Web版スタティックライブラリのビルド
今回はOpenSiv3D (たぶん)v0.4.1時点のmasterブランチコードを元に作業しました。
https://github.com/RYOSKATE/OpenSiv3D/tree/web
(web
という名前でブランチを切って作業しました。)
まずはこれをEmscriptenでビルドしスタティックライブラリにすることを目指します。
始めは、わかりやすいところから攻めようと思い
・Siv3D\include\Siv3D\Platform.hpp
・Siv3D\include\Siv3D\PlatformDetail.hpp
にWebブラウザ版用に以下のような変更、修正をしました。(一部抜粋,省略)
//Platform.hpp
# define SIV3D_PLATFORM_PRIVATE_DEFINITION_WEB() 0 //
//Platform.hpp
# if defined(__EMSCRIPTEN__)
# define SIV3D_PLATFORM_NAME U"Web"
# undef SIV3D_PLATFORM_PRIVATE_DEFINITION_WEB
# define SIV3D_PLATFORM_PRIVATE_DEFINITION_WEB() 1
//Platform.hpp
# elif SIV3D_PLATFORM(WEB)
# undef SIV3D_WITH_FEATURE_PRIVATE_DEFINITION_SSE2
# define SIV3D_WITH_FEATURE_PRIVATE_DEFINITION_SSE2() 1
//PlatformDetail.hpp
# if SIV3D_PLATFORM(WEB)
# include <emscripten.h>
# endif
//PlatformDetail.hpp
# elif SIV3D_PLATFORM(WEB)
inline uint64 Rdtsc()
{
return static_cast<int64_t>(emscripten_get_now() * 1e+6);
}
Siv3DMain.cppのコンパイル
次にSiv3D/src/Siv3D-Platform/Linux
ディレクトリをコピー&リネームして、Siv3D/src/Siv3D-Platform/Web
を作りました。
調べて見るとOpenSiv3DはSiv3D/src/Siv3D-Platform/Web/Siv3DMain.cpp
のように、各環境毎に用意されたmain関数入りソースファイルを用意しているようです(参考)。まずはこのファイルをコンパイルしてみました。
ただし、当然そのままではerror: undefined symbol:
が多発するため、main関数の中身は全てコメントアウトしています。
emcc Siv3D\src\Siv3D-Platform\Web\Siv3DMain.cpp -std=c++17 -ISiv3D/include -ISiv3D/src/Siv3D -ISiv3D/src/Siv3D-Platform/Web
これでビルドは出来たのですが、ここから色々と対象ファイルが増えてくるとemccで個々にやるのは無理があります。やはりMakefileを作りたくなりました。
CMakeLists.txtの作成
始めはCmakeListsというものを知らなかったため、自力でMakefileを作り試行錯誤していました。
が、やはり0から作るのはしんどかったので、Linux版のCmakeListsを利用するようにしました。
makeとcmakeが必要そうなので、インストールしました。
私はchocolateyというWindows用のパッケージ管理ツールを使っていたため、
choco install make
choco install cmake
で、インストールしたところ、結果的には動いたので、たぶん大丈夫です。
不安な方は公式のインストーラを使用しましょう。
そして、最上位にWebというディレクトリを作り、その中にCmakeLists.txtとbuildディレクトリを作成しました。
CmakeLists.txtはLinux版を参考に、コンパイラなどの設定をいくつか変更し、以下のようになりました。
cmake_minimum_required(VERSION 2.8)
#find_package(PkgConfig)
project(OpenSiv3D_Web CXX)
set(CMAKE_BUILD_TYPE Debug)
#set(CMAKE_BUILD_TYPE Release)
set(CMAKE_CXX_COMPILER "em++")
set(CMAKE_CXX_FLAGS "-std=c++17 -Wall -Wextra -Wno-unknown-pragmas -fPIC -msse4.1 -D_GLFW_X11")
set(CMAKE_CXX_FLAGS_DEBUG "-g3 -O0 -pg -DDEBUG")
set(CMAKE_CXX_FLAGS_RELEASE "-O2 -DNDEBUG -march=x86-64")
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-g3 -Og -pg")
set(CMAKE_CXX_FLAGS_MINSIZEREL "-Os -DNDEBUG -march=x86-64")
set(CMAKE_C_COMPILER "emcc")
#set(CMAKE_C_COMPILER "gcc")
set(CMAKE_C_FLAGS "-Wall -Wextra -Wno-unknown-pragmas -fPIC -msse4.1 -D_GLFW_X11")
set(CMAKE_C_FLAGS_DEBUG "-g3 -O0 -pg -DDEBUG")
set(CMAKE_C_FLAGS_RELEASE "-O2 -DNDEBUG -march=x86-64")
set(CMAKE_C_FLAGS_RELWITHDEBINFO "-g3 -Og -pg")
set(CMAKE_C_FLAGS_MINSIZEREL "-Os -DNDEBUG -march=x86-64")
include_directories(
"../Siv3D/include"
"../Siv3D/src/Siv3D"
"../Siv3D/src/Siv3D-Platform/Web"
"../Siv3D/include/ThirdParty"
"../Siv3D/src/ThirdParty"
${LIBSIV3D_INCLUDE_DIRS}
)
set(SOURCE_FILES
"../Siv3D/src/Siv3D-Platform/Web/Siv3DMain.cpp"
)
add_library(Siv3D STATIC ${SOURCE_FILES})
target_link_libraries(Siv3D
)
OpenSiv3D/Web/build ディレクトリで以下のコマンドを叩くとMakefileが生成されます。
Linux版はninjaを使用しておりWeb版もninjaを使いたかったのですが、私はどうも設定がうまくいかずできなかったため素直にMakefileを生成しています。
emcmake cmake -DCMAKE_BUILD_TYPE=Release -G "Unix Makefiles" ..
(私は始めから入っていたためそのまま動いてしまいましたが、もしかしたらMinGWなどの追加インストールが必要かもしれません。また、もし上記のコマンドがうまくいかず、MinGWなどをインストールした後に再実行する場合は、生成されたファイルを一度全て削除してからでないとうまく動かないことがあるので注意しましょう)
上記コマンドの実行がうまくいけば
-- Configuring done
-- Generating done
-- Build files have been written to: C:/OpenSiv3D/Web/build
のように表示され、build以下にMakefielが生成されているはずです。
そのままmakeと叩けばビルドされ、libSiv3D.a
が生成されます。
3. Web版アプリのビルド
ひとまず、中身空っぽで何も意味は無いですがEmscriptenでスタティックライブラリを作れました。次は、これをリンクしてWeb版のアプリを作ります。
buildと同じ階層にAppというディレクトリを作成し、その中に
- Main.cpp
- CMakeLists.txt
というファイルを作成します。
//Main.cpp
#include <Siv3D.hpp>
#include <emscripten.h>
#include <iostream>
void Main()
{
std::cout << "Hello\n";
}
cmake_minimum_required(VERSION 2.8)
#find_package(PkgConfig)
project(OpenSiv3D_Web CXX)
set(CMAKE_BUILD_TYPE Debug)
#set(CMAKE_BUILD_TYPE Release)
set(CMAKE_CXX_COMPILER "em++")
set(CMAKE_CXX_FLAGS "-std=c++17 -s WASM=1")
set(CMAKE_CXX_FLAGS_DEBUG "-g3 -O0 -pg -DDEBUG")
set(CMAKE_CXX_FLAGS_RELEASE "-O2 -DNDEBUG -march=x86-64")
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-g3 -Og -pg")
set(CMAKE_CXX_FLAGS_MINSIZEREL "-Os -DNDEBUG -march=x86-64")
set(CMAKE_C_COMPILER "emcc")
#set(CMAKE_C_COMPILER "gcc")
set(CMAKE_C_FLAGS "-Wall -Wextra -s WASM=1")
set(CMAKE_C_FLAGS_DEBUG "-g3 -O0 -pg -DDEBUG")
set(CMAKE_C_FLAGS_RELEASE "-O2 -DNDEBUG -march=x86-64")
set(CMAKE_C_FLAGS_RELWITHDEBINFO "-g3 -Og -pg")
set(CMAKE_C_FLAGS_MINSIZEREL "-Os -DNDEBUG -march=x86-64")
set(CMAKE_EXECUTABLE_SUFFIX ".html")
include_directories(
"../../Siv3D/include"
"../../Siv3D/include/ThirdParty"
)
set(SOURCE_FILES
"./Main.cpp"
)
add_executable(Siv3D_App ${SOURCE_FILES})
target_link_libraries(Siv3D_App
${PROJECT_SOURCE_DIR}/../Build/libSiv3D.a
)
Appディレクトリ内で以下のコマンドを実行し、Makefile生成後にmakeを実行します。
emcmake cmake -DCMAKE_BUILD_TYPE=Release -G "Unix Makefiles" .
(先程のbuildと異なり、最後は.
なので注意)
うまくいけば、Siv3D_App.html
が生成されます。Chromeなどで開きましょう。
ただし、ローカルファイルを開くとブラウザのファイル読み込み制限の関係でうまくいかないようです。
手っ取り早い方法としては、EmscriptenインストールでPythonも入っていることですしAppディレクトリで
python -m SimpleHTTPServer 8080
と、サーバを立ち上げればhttp://localhost:8080/Siv3D_App.html
でアクセスできます。
うまくいけば、Emscriptenデフォルトの黒いウィンドウが確認できます。
4. 機能の追加
ここまでくれば、あとは一つずつ動かしたい機能を使用する処理を書き、必要なソースファイルを含めてビルドするだけです。
とりあえず、かつてのOpenSiv3Dの始まりにならってArray
とString
あたりから私は手を付けました。
App側のMain関数内でArray<int> arr = {1,2,3};
のように簡単な処理を追加して、ビルドしてみます。
In file included from C:\emsdk\upstream\lib\clang\10.0.0\include\emmintrin.h:13:
In file included from C:\emsdk\upstream\lib\clang\10.0.0\include\xmmintrin.h:13:
C:\emsdk\upstream\lib\clang\10.0.0\include\mmintrin.h:33:5: error: use of undeclared identifier '__builtin_ia32_emms'; did you mean '__builtin_isless'?
__builtin_ia32_emms();
^
C:\emsdk\upstream\lib\clang\10.0.0\include\mmintrin.h:33:5: note: '__builtin_isless' declared here
C:\emsdk\upstream\lib\clang\10.0.0\include\mmintrin.h:33:25: error: too few arguments to function call, expected 2, have 0
__builtin_ia32_emms();
^
C:\emsdk\upstream\lib\clang\10.0.0\include\mmintrin.h:50:19: error: use of undeclared identifier '__builtin_ia32_vec_init_v2si'
return (__m64)__builtin_ia32_vec_init_v2si(__i, 0);
^
C:\emsdk\upstream\lib\clang\10.0.0\include\mmintrin.h:67:12: error: use of undeclared identifier '__builtin_ia32_vec_ext_v2si'
return __builtin_ia32_vec_ext_v2si((__v2si)__m, 0);
^
C:\emsdk\upstream\lib\clang\10.0.0\include\mmintrin.h:129:19: error: use of undeclared identifier '__builtin_ia32_packsswb'
return (__m64)__builtin_ia32_packsswb((__v4hi)__m1, (__v4hi)__m2);
^
C:\emsdk\upstream\lib\clang\10.0.0\include\mmintrin.h:159:19: error: use of undeclared identifier '__builtin_ia32_packssdw'
return (__m64)__builtin_ia32_packssdw((__v2si)__m1, (__v2si)__m2);
^
C:\emsdk\upstream\lib\clang\10.0.0\include\mmintrin.h:189:19: error: use of undeclared identifier '__builtin_ia32_packuswb'
return (__m64)__builtin_ia32_packuswb((__v4hi)__m1, (__v4hi)__m2);
^
C:\emsdk\upstream\lib\clang\10.0.0\include\mmintrin.h:216:19: error: use of undeclared identifier '__builtin_ia32_punpckhbw'
return (__m64)__builtin_ia32_punpckhbw((__v8qi)__m1, (__v8qi)__m2);
^
C:\emsdk\upstream\lib\clang\10.0.0\include\mmintrin.h:239:19: error: use of undeclared identifier '__builtin_ia32_punpckhwd'
return (__m64)__builtin_ia32_punpckhwd((__v4hi)__m1, (__v4hi)__m2);
^
C:\emsdk\upstream\lib\clang\10.0.0\include\mmintrin.h:260:19: error: use of undeclared identifier '__builtin_ia32_punpckhdq'
return (__m64)__builtin_ia32_punpckhdq((__v2si)__m1, (__v2si)__m2);
^
C:\emsdk\upstream\lib\clang\10.0.0\include\mmintrin.h:287:19: error: use of undeclared identifier '__builtin_ia32_punpcklbw'
return (__m64)__builtin_ia32_punpcklbw((__v8qi)__m1, (__v8qi)__m2);
^
C:\emsdk\upstream\lib\clang\10.0.0\include\mmintrin.h:310:19: error: use of undeclared identifier '__builtin_ia32_punpcklwd'
return (__m64)__builtin_ia32_punpcklwd((__v4hi)__m1, (__v4hi)__m2);
^
C:\emsdk\upstream\lib\clang\10.0.0\include\mmintrin.h:331:19: error: use of undeclared identifier '__builtin_ia32_punpckldq'
return (__m64)__builtin_ia32_punpckldq((__v2si)__m1, (__v2si)__m2);
^
C:\emsdk\upstream\lib\clang\10.0.0\include\mmintrin.h:352:19: error: use of undeclared identifier '__builtin_ia32_paddb'
return (__m64)__builtin_ia32_paddb((__v8qi)__m1, (__v8qi)__m2);
^
C:\emsdk\upstream\lib\clang\10.0.0\include\mmintrin.h:373:19: error: use of undeclared identifier '__builtin_ia32_paddw'
return (__m64)__builtin_ia32_paddw((__v4hi)__m1, (__v4hi)__m2);
^
C:\emsdk\upstream\lib\clang\10.0.0\include\mmintrin.h:394:19: error: use of undeclared identifier '__builtin_ia32_paddd'
return (__m64)__builtin_ia32_paddd((__v2si)__m1, (__v2si)__m2);
^
C:\emsdk\upstream\lib\clang\10.0.0\include\mmintrin.h:416:19: error: use of undeclared identifier '__builtin_ia32_paddsb'
return (__m64)__builtin_ia32_paddsb((__v8qi)__m1, (__v8qi)__m2);
^
C:\emsdk\upstream\lib\clang\10.0.0\include\mmintrin.h:439:19: error: use of undeclared identifier '__builtin_ia32_paddsw'
return (__m64)__builtin_ia32_paddsw((__v4hi)__m1, (__v4hi)__m2);
^
fatal error: too many errors emitted, stopping now [-ferror-limit=]
20 errors generated.
うわーー
SIMD演算
エラーの原因である mmintrin.h
, xmmintrin.h
, emmintrin.h
などはSIMD演算関係のヘッダのようです。
が、どうやらEmscriptenは -msse4.1
のようなコンパイルオプションを渡すだけでは駄目なようです。
Porting SIMD code targeting WebAssembly
#include <wasm_simd128.h>
というヘッダを代わりに使い、-msimd128
というオプションを渡してあげれば良さそうな雰囲気があります。
というわけで、以下のようなヘッダファイルを作成し、xmmintrin.h
, emmintrin.h
をincludeしている箇所を全てwasm_simd128.h
に置き換えました。__m128
は定義がなさそうだったので、コピペしています。
//MySSE.h
# pragma once
#include "Platform.hpp"
#if SIV3D_PLATFORM(WEB)
# include <emscripten.h>
# include <wasm_simd128.h>
# include <Siv3D/Types.hpp>
union __m128 {
float m128_f32[4];
s3d::uint64 m128_u64[2];
s3d::int8 m128_i8[16];
s3d::int16 m128_i16[8];
s3d::int32 m128_i32[4];
s3d::int64 m128_i64[2];
s3d::uint8 m128_u8[16];
s3d::uint16 m128_u16[8];
s3d::uint32 m128_u32[4];
};
union __m128i {
s3d::int8 m128i_i8[16];
s3d::int16 m128i_i16[8];
s3d::int32 m128i_i32[4];
s3d::int64 m128i_i64[2];
s3d::uint8 m128i_u8[16];
s3d::uint16 m128i_u16[8];
s3d::uint32 m128i_u32[4];
s3d::uint64 m128i_u64[2];
};
#else
# include <emmintrin.h>
# include <xmmintrin.h>
#endif
該当するのはSIMDMath.hpp
, SIMD_Float4.hpp
, SFMT.hpp
あたりで、上記MySSE.hpp
をincludeするように変更していきました。
しかしそれでも、まだ同様のエラーが発生しました。
調べてみると、同じようなところで困っている例を見つけました。
Compiling Issues when using Xmmintrin.h
どうやら、最初にインストールしたlatest
なEmscripteではこのあたりはまだ未対応のようです。latest-upstream
を使ってみます。以下のコマンドを叩き、2杯目のタピオカでも飲んで待ちましょう。
emsdk install latest-upstream
emsdk activate latest-upstream
インストール後、再びmakeしてみます。
C:/develop/OpenSiv3D/Web/../Siv3D/include\Siv3D/SIMDMath.hpp:205:11: error: use of undeclared identifier '_mm_setzero_ps'
return _mm_setzero_ps();
^
C:/develop/OpenSiv3D/Web/../Siv3D/include\Siv3D/SIMDMath.hpp:225:11: error: use of undeclared identifier '_mm_set_ps'
return _mm_set_ps(w, z, y, x);
^
C:/develop/OpenSiv3D/Web/../Siv3D/include\Siv3D/SIMDMath.hpp:230:25: error: use of undeclared identifier '_mm_set_epi32'
const __m128i temp = _mm_set_epi32(static_cast<int>(w), static_cast<int>(z), static_cast<int>(y), static_cast<int>(x));
^
C:/develop/OpenSiv3D/Web/../Siv3D/include\Siv3D/SIMDMath.hpp:232:11: error: use of undeclared identifier '_mm_castsi128_ps'
return _mm_castsi128_ps(temp);
(中略)
少し進んだ感じはありますが、まだ駄目でした🙄
覚悟をキメました。足りない部分は気合で実装します。
//MySSE.h
inline __m128 _mm_set_ss(float w )
{
return { w, 0.0f, 0.0f, 0.0f };
}
inline __m128 _mm_set1_ps(float w )
{
return { w, w, w, w };
}
inline __m128 _mm_set_ps(float z, float y, float x, float w )
{
return { w, z, y, x };
}
inline __m128 _mm_setr_ps (float z, float y, float x, float w )
{
return { x, y, z, w };
}
inline __m128 _mm_setzero_ps(void)
{
return { 0.0f, 0.0f, 0.0f, 0.0f };
}
inline __m128i _mm_set_epi32(int _I3, int _I2, int _I1, int _I0)
{
return {.m128i_i32 = {_I3, _I2, _I1, _I0}};
}
inline __m128 _mm_castsi128_ps(__m128i a) {
return {.m128_i64 = {a.m128i_i64[0], a.m128i_i64[1]}};
}
inline __m128 _mm_set_ps1(float _A) {
return _mm_set1_ps(_A);
}
inline __m128i _mm_set1_epi32(int i) {
return {.m128i_i32 = {i,i,i,i}};
}
inline __m128 _mm_shuffle_ps(__m128 _A, __m128 _B, unsigned int _Imm8) {
int z = _Imm8 & 0b11000000;
int y = _Imm8 & 0b00110000;
int x = _Imm8 & 0b00001100;
int w = _Imm8 & 0b00000011;
return {.m128_i32 = {
_B.m128_i32[z],
_B.m128_i32[y],
_A.m128_i32[x],
_A.m128_i32[w]}};
}
#define _MM_SHUFFLE(fp3,fp2,fp1,fp0) (((fp3) << 6) | ((fp2) << 4) | \
((fp1) << 2) | ((fp0)))
inline float _mm_cvtss_f32(__m128 _A) {
return _A.m128_f32[0];
}
inline __m128i _mm_castps_si128(__m128 _A) {
return {.m128i_i64 = {_A.m128_i64[0], _A.m128_i64[1]}};
}
inline int _mm_cvtsi128_si32(__m128i _A) {
return _A.m128i_i32[0];
}
inline __m128 _mm_move_ss( __m128 a, __m128 b){
return { b.m128_f32[0], a.m128_f32[1], a.m128_f32[2], a.m128_f32[3] };
}
inline __m128 _mm_load_ss(float const*_A) {
return { *_A, 0,0,0 };
}
inline void _mm_store_ss(float *_V, __m128 _A) {
*_V = _A.m128_f32[0];
}
...(中略)
というわけで、ビルドが通るようになりました。
このあたりで、さすがに「サクッとお手軽にWebブラウザ版が作れるのではないか」という考えは間違いだったことに気が付き始めました。
Siv3DEngine
ArrayやStringを素朴にstd::coutするようなプログラムが動き、最初期のOpenSiv3DをMacで動かしたときに近い感動を得られました。
すると、次はやはり2Dの描画をできるようにしたいと思いました。
ここから、Siv3DMain.cppのコメントアウトをちょっとずつ解除していきながら、関連するソースファイルをビルドしていく作業を始めました。
まずはSiv3DEngineです。長いのでこちらには貼りませんが、Logger, System, Windows, Graphics以外のクラスはコメントアウトしておきます。
class ISiv3DLogger;
class ISiv3DSystem;
class ISiv3DWindow;
class ISiv3DGraphics;
(中略)
std::tuple<
Siv3DComponent<ISiv3DLogger>
, Siv3DComponent<ISiv3DSystem>
, Siv3DComponent<ISiv3DWindow>
, Siv3DComponent<ISiv3DGraphics>
> m_interfaces;
それぞれのクラスのソースファイルを、動かせるように修正していきます。
長くなるので、ここでは概要だけ紹介します。詳細はリポジトリのソースコードを見ていただければと思います。
CLogger.cpp
ログのHTMLファイルなど、ファイル出力関係の関数の中身は全てコメントアウトします。
TRACE_LOG
などのデバッグログは標準出力で見られれば、ひとまずは十分です。
まだ実現できていませんが、事前にファイル名が確定しているログファイルなどであればたぶん実現できそうな気はします。
CSystem.cpp
init()
内では
Siv3DEngine::Get<ISiv3DWindow>()->init();
Siv3DEngine::Get<ISiv3DGraphics>()->init();
以外のクラスの->init()
、及びその後の動かなそうなところはひとまずコメントアウトしてしまいます。
CWindow.cpp
この時点ではなんだかよくわかっていませんが、::glfw
から始まる部分は全てコメントアウトしていまいます。(まずは枠組みだけ動けばいいのです)
CGraphics_GL.cpp
だいたいの関数の中身をコメントアウトします。(まずはry)
これらと関連するソースファイルをCMakeList.txtのset(SOURCE_FILES
に追加していきます。
"../Siv3D/src/Siv3D/Window/WindowFactory.cpp"
"../Siv3D/src/Siv3D/Window/SivWindow.cpp"
"../Siv3D/src/Siv3D-Platform/Web/Window/CWindow.cpp"
のようにSiv3D直下とSiv3D-Platform/Webの両方に関連するソースファイルがあるので忘れずに追加しましょう。
Siv3DMain.cpp
のSiv3DEngine engine;
、Siv3DEngine::Get<ISiv3DSystem>()->init();
のコメントアウトを解除して、build, Appでmakeしてみると以下の画像のようにそれっぽいデバッグログが出力されるようになります。
画面出力(OpenGL)
ここまでくると、左側の黒い領域にRectやCircleを描画したくなってしまうのがSiv3Derというものです。
Linux版のOpenSiv3Dは描画処理などにOpenGLというライブラリを使っているようです。
幸運にもEmscriptenはOpenGLをWebGLに変換することでサポートしているようです。
OpenGL support in Emscripten
CWindow.hppやCWindow.cppなどで
# include <GL/glew.h>
# include <GLFW/glfw3.h>
して、::glfw
で始まる関数を呼び出しているのが、OpenGL関連の処理のようです。
これらの処理のコメントアウトを少しずつ、動く範囲で解除していきます。
数箇所ほど解除して、ビルドを試みます。
このあたりでOpenGLの処理の流れを1%くらい理解してきました。
EmscriptenではGLとGLFWは標準で用意されておりCMAKE_CXX_FLAGSに -s FULL_ES2=1 -s USE_GLFW=3 -s USE_WEBGL2=1
というオプションを追加すれば、よしなにリンクしてくれるようです。
Siv3D/src/ThirdParty/GLFW
とSiv3D/src/ThirdParty/GL
は標準で用意されているものと競合するため、一旦削除します。
ファイルの読み込み(シェーダ)
m_copyProgram = detail::LoadShaders(Resource(U"engine/shader/fullscreen_triangle.vert"), Resource(U"engine/shader/fullscreen_triangle.frag"));
のようにシェーダファイルを読み込んでいる処理があります。
Emscriptenではブラウザの制約で読み書きするファイルはビルド時に指定しておかなければいけないようです。数が多くなりそうな気がするので、CmakeLists.txtではなく別ファイルで管理したいと思い、調査したところ以下のような方法を見つけました。
Emscriptenでファイルを読み込むソースをJavaScriptに変換するには
Module['preRun'] = function () {
FS.createFolder(
'/', // 親フォルダの指定
'resources', // フォルダ名
true, // 読み込み許可
true // 書き込み許可(今回の例はfalseでもよさげ)
);
FS.createFolder(
'/resources',
'engine',
true,
true
);
FS.createFolder(
'/resources/engine',
'shader',
true,
true
);
FS.createPreloadedFile(
'/resources/engine/shader',
'fullscreen_triangle.frag', // ソース中でのファイル名
'/resources/engine/shader/fullscreen_triangle.frag', // httpでアクセスする際のURLを指定
true,
false
);
FS.createPreloadedFile(
'/resources/engine/shader',
'fullscreen_triangle.vert',
'/resources/engine/shader/fullscreen_triangle.vert',
true,
false
);
};
という内容でpre.js
をAppディレクトリに作成します。
CMAKE_CXX_FLAGSに --pre-js pre.js
と追加してあげれば、jsファイルに記録したファイルの読み書きができるようになります。
この時点でビルドすると
C:/OpenSiv3D/Siv3D/src/Siv3D-Platform/Web/Window/CWindow.cpp:76:5: error: no member named 'glfwInitHint' in the global namespace
::glfwInitHint(GLFW_COCOA_CHDIR_RESOURCES, GLFW_FALSE);
~~^
C:/OpenSiv3D/Siv3D/src/Siv3D-Platform/Web/Window/CWindow.cpp:76:18: error: use of undeclared identifier 'GLFW_COCOA_CHDIR_RESOURCES'
::glfwInitHint(GLFW_COCOA_CHDIR_RESOURCES, GLFW_FALSE);
::glfwInitHint
がエラーを起こしていました。
何やら、嫌な予感がします。SIMD演算のときを思い出して震えてきます。
とりあえず、::glfwInitHint
は再びコメントアウトしてからビルドすると、
シェーダファイルの読み込みもしており、それっぽい感じになっています。
メインループ
そろそろApp側もSiv3Dっぽくしていく必要がありそうです。
残念ながら以下のようなコードをEmscriptenでコンパイルしてみると、ブラウザがハングアップしてしまいました。どうやらEmscriptenではwhileで無限ループさせるコードを実行すると、ブラウザの画面処理がされなくなってしまい、ハングアップしてしまうようです。
// Main.cpp
void Main()
{
while(System::Update())
{
}
}
代わりにemscripten_set_main_loop
という関数を使うしか無いようです。
void mainLoop() {
if (System::Update()) {
if (counter++ % 60 == 0) {
//1秒ごとに数字をログ出力するだけです。意味はありません。
printf("counter %p: %03d\n", &counter, counter);
}
} else {
emscripten_cancel_main_loop();
emscripten_force_exit(0);
}
}
void Main()
{
emscripten_set_main_loop(mainLoop, 0, true);
}
ちなみに、私の環境では最初emscripten_set_main_loopがエラーを起こし上手く動きませんでした。
その時は、代わりになぜか動くemscripten_async_callを使っていました。
void mainLoop(void *) {
if (counter++ % 60 == 0) {
printf("counter %p: %03d\n", &counter, counter);
}
if (System::Update()) {
emscripten_async_call(mainLoop, nullptr, 1000 / 60);
} else {
emscripten_force_exit(0);
}
}
void Main()
{
mainLoop(nullptr);
}
動かなかったのはOpenGL周りのコンパイル設定や初期化が不十分だったせいかもしれません。
色々とコメントアウトを解除した頃に試したらemscripten_set_main_loopも動くようになっていました。
図形の描画
メインループも用意できたことで、Circle(100,100,50).draw();
などを動かしたくなってきました。
Main.cppに処理を追加してビルドしてみます。
[ 50%] Linking CXX executable Siv3D_App.html
error: undefined symbol: glCreateShaderProgramv
warning: To disable errors for undefined symbols use `-s ERROR_ON_UNDEFINED_SYMBOLS=0`
Error: Aborting compilation due to previous errors
shared:ERROR: 'C:/emsdk/node/12.9.1_64bit/bin/node.exe C:\emsdk\upstream\emscripten\src\compiler.js c:\users\RYOSKATE\appdata\local\temp\tmp0rmj1s.txt' failed (1)
glCreateShaderProgramv
という関数が上手くリンクできていないようです。
-s FULL_ES2=1 -s USE_GLFW=3 -s USE_WEBGL2=1
以外にもオプションがないか調査したり、
#define GLEW_STATIC
#define GLFW_INCLUDE_GLEXT
#define GLFW_INCLUDE_ES3
#define GL_GLEXT_PROTOTYPES
#define EGL_EGLEXT_PROTOTYPES
#define GLFW_INCLUDE_ES31
など、ヘッダの中身を見てそれっぽい#defineを色々と組み合わせたりしても、どうにもうまくいきません。
glCreateShaderProgramv
のリファレンスを調べてみると、
https://www.khronos.org/opengl/wiki/GLAPI/glCreateShaderProgram
この関数はOpenGL 4.1から追加されたようですね。何やらとても嫌な予感がします。
改めて、Emscriptenの説明を読んでみます。
https://emscripten.org/docs/porting/multimedia_and_graphics/OpenGL-support.html
By default, Emscripten targets the WebGL-friendly subset of OpenGL ES 2.0
ESとはなんでしょうか。ここにきて、OpenGLにも種類があることに気が付きます。
結論から言うと、EmscriptenはOpenGL 2.0の一部の機能をサポートしているようです。つまり4.1の機能などがサポートされるのはまだまだ先になりそうです。永遠に来ないかも知れません。
ただし、諦めるのは早いです。
リファレンスを見てみると
glCreateShaderProgram is equivalent (assuming no errors are generated) to:
const GLuint shader = glCreateShader(type);
if (shader) {
glShaderSource(shader, count, strings, NULL);
glCompileShader(shader);
const GLuint program = glCreateProgram();
if (program) {
GLint compiled = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
glProgramParameteri(program, GL_PROGRAM_SEPARABLE, GL_TRUE);
if (compiled) {
glAttachShader(program, shader);
glLinkProgram(program);
glDetachShader(program, shader);
}
/* append-shader-info-log-to-program-info-log */
}
glDeleteShader(shader);
return program;
} else {
return 0;
}
のように、代替実装が紹介されており、頑張ればなんとかなりそうな道筋が一瞬見えました。
が、
glGenProgramPipelines
glDeleteProgramPipelines
glPolygonMode
glBindProgramPipeline
glUseProgramStages
などなど、equivalentな実装が見当たらないものもありました。
おわりに
というところで進捗は止まってしまっています。
まず、このまま進めるなら上記のequivalentな実装が見当たらないOpenGLの処理を自力で実装しなければなりませんが、OpenGLを2%くらいしか理解できていない自分ではとても難しい状況です。
また、今回私はかなり機能が増えてきた最近のOpenSiv3Dのコードをベースに、動かせないところはコメントアウトして無理やり動かそうとするアプローチを取ってしまいましたが、最新版は図形の影や物理演算などの処理も混じっており、このあたりはいきなりの実装は当然難しいとして、どうコメントアウトしたりバイパスさせればいいのかだけでもOpenSiv3Dを完全に理解している人でないと厳しいと思いました。
もしWebブラウザ版の開発をするなら、2D図形の素朴な描画機能が入り始めた頃のバージョンのコードを使用して、EmscriptenがサポートしているOpenGL ESの機能で実装を追いかけるアプローチを取ってあげたほうが良いかなと思いました。
以上が、進捗の軌跡と言う名の紆余曲折です。
3行くらいで書いてあるエラーの対処も、たぶん実際には数日かかった記憶があります。
Webブラウザ版の開発を試みる人が現れたら、同じ苦しみを味わわないよう少しでも役に立てば幸いです。
これを読んだあなた。どうかWebブラウザ版の開発にチャレンジしてください。
それだけが私の望みです。
追記) どうやら本当にチャレンジされた方がおり、素晴らしいことにWeb版の開発が実現しているようです。
OpenSiv3D Web版を使ってブラウザで動くゲームを作ってみる (Visual Studio版)