Help us understand the problem. What is going on with this article?

OpenSiv3DのWebブラウザ版をEmscriptenで作ろうとした話

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

で、インストールしたところ、結果的には動いたので、たぶん大丈夫です。

不安な方は公式のインストーラを使用しましょう。
* http://www.cmake.org/download/
* http://www.mingw.org/wiki/getting_started

そして、最上位に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デフォルトの黒いウィンドウが確認できます。

無題.png

4. 機能の追加

ここまでくれば、あとは一つずつ動かしたい機能を使用する処理を書き、必要なソースファイルを含めてビルドするだけです。
とりあえず、かつてのOpenSiv3Dの始まりにならってArrayStringあたりから私は手を付けました。

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.cppSiv3DEngine engine;Siv3DEngine::Get<ISiv3DSystem>()->init();のコメントアウトを解除して、build, Appでmakeしてみると以下の画像のようにそれっぽいデバッグログが出力されるようになります。

無題2.png

画面出力(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/GLFWSiv3D/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は再びコメントアウトしてからビルドすると、

image.png

シェーダファイルの読み込みもしており、それっぽい感じになっています。

メインループ

そろそろApp側もSiv3Dっぽくしていく必要がありそうです。
残念ながら以下のようなコードをEmscriptenでコンパイルしてみると、ブラウザがハングアップしてしまいました。どうやらEmscriptenではwhileで無限ループさせるコードを実行すると、ブラウザの画面処理がされなくなってしまい、ハングアップしてしまうようです。

// Main.cpp
void Main()
{
 while(System::Update())
 {
 }
}

https://emscripten.org/docs/api_reference/emscripten.h.html#browser-execution-environment

代わりに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

image.png

この関数は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にも種類があることに気が付きます。

https://ja.wikipedia.org/wiki/OpenGL_ES

結論から言うと、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ブラウザ版の開発にチャレンジしてください。
それだけが私の望みです。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away