【Unity】WebGLでのJavaScript<=>C#の連携

close up photo of programming of codes Unity
Photo by luis gomes on Pexels.com
この記事は約12分で読めます。

UnityでWebGLビルドした場合のJavaScriptとC#の連携についてのメモです。emscriptenやUnityにはあまり詳しくないので、詳細は公式ドキュメントを参照ください。

環境

  • Unity 2021.3.5f1
  • macOS 10.15.7

emscripten

emscriptenは、UnityのWebGLビルドで使われているコンパイラです。Unityのスクリプトは、c#->c++->emscripten (wasm)の流れで、WebGL向けのプログラムが生成されるとdocsには書かれています。最終的には、JavaScriptとwasm、dataファイル等が生成されますが、emscriptenを少し触るとUnityのWebGLビルドの出力についてある程度イメージができるようになると思います。そのため、まずはemscriptenについて以下見ていきたいと思います。

参考リンク

最初にemscriptenを単体でインストールします。

簡単なサンプルを実行してみましょう。公式docにあるサンプルです。

ビルドしてみましょう。

続いてemrunコマンドでfunction.htmlをブラウザでオープンします。

ブラウザコンソールでCの関数を呼んでみます。

ビルドで生成されたfunctions.jsを見てみますと以下のようなコードが展開されています。

cwrap関数はccallをコールする無名関数を返しています。Moduleは、外部とのインターフェースに用いられるオブジェクトで、ccallcwrap関数が登録されていることが分かります。

mergeInto

Unityでは、.jslib内でmergeInto関数を使って、C#からJavaScript関数を呼べるようになります(詳細には、xxx.framework.jsに.jslibで定義したコードがビルドされ展開されています)。

emscriptenでmergeIntoというインターフェースを提供しているようですので使用例を見てみます。

cpp側です。

js側です。LibraryManager.libraryにマージします。

以下コマンドでビルドします。--js-libraryというオプションでJavaScriptファイルを指定します。

ビルド後のスクリプトを見てみます。

mergeIntoで定義した関数は、_(アンダーバー)付きの関数として展開されていることが分かります。

一方、my_js2関数がビルド後のスクリプトに見当たりません。docsを見ると、emscriptenはスペース節約のためc/c++から参照されるプロパティのみを含むとありました(おそらくその影響?)

参考リンク:https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html?highlight=mergeinto#javascript-limits-in-library-files

prejs、postjs

mergeInto以外にも--pre-js--post-jsという方法もあるようです。

ビルドします。

結果を見ると以下のようになっています。

postjsの場合は以下のようになりました(--post-jsオプション指定します)。スクリプトの最後尾に展開されているようです。

__postset

__postsetを使うとmergeIntoの展開後に関数を実行するといったことが実現できるようです。

__deps

__depsを使うと依存関数を展開できます。

展開後は以下のようになりました。$をつけたプロパティは、ビルド後に展開されたスクリプト内では$が外れています。

dynCall

dynCallでポインタ経由でネイティブコードを呼び出せます。

Unity jslibとjspre

emscriptenでのJavaScriptコードの指定方法がある程度分かったので、続いてUnity側を見ていきます。

Unityでは、JavaScriptコードをビルド対象に含めるために.jslibまたは.jspreの拡張子を持ったファイルを準備します。emscriptenのビルドで見たように、.jslib--js-library.jspre--pre-jsに相当してそうです。

参考リンク

jslibとjspreを使ったサンプルを作成してみます。

C#のスクリプトは以下のようなサンプルでやってみます。jslibとjspreをそれぞれ作成しています。

DllImportは、C#スクリプトからJavaScript側のプログラムを呼ベるようにするための指定です。DllImportEntryPointはビルド後のリンクするシンボル名として渡せそうです。何も指定しないとmergeIntoのプロパティ名をシンボル名として探すようです。上記では、C#からHogeHogeという関数名でJavaScript側のHello関数を呼ぶように指定しています。実際に動かすとちゃんとビルドもパスし関数を呼び出せています。

上記でWebGLビルドした際に生成されるframework.jsを少し覗いてみます。

まず構造は以下のようになっています。

emscriptenのビルドで生成されたスクリプトと同じ構造になっており、Unityの場合はunityFrameworkという関数でwrapされていることが分かります。

生成されたhtmlをブラウザで開いてみます。Buildという名前でビルド先を指定したとします。

http://localhost:8000 でアクセスしてみます。関数が実行されていることが分かりました。

C#からJavaScriptのコードを実行できているようです。

WebGLのビルドで、C#スクリプトはil2cppでは以下のようなcppコードに展開されているようです。先のHogeHoge関数は、内部でHello関数を実行してます。

JavaScriptからC#関数を呼ぶ

続いてJavaScriptからC#の関数を呼ぶ方法について試行していきます。

emcscriptenやUnityのWebGLビルドでは、wasmでC/C++のコードを呼ぶためのUtility関数がビルド後のスクリプト内で使用できるようになっています。この関数インターフェースを使用することで、UnityのC#スクリプトを呼ぶことができそうです。

Module関数

Moduleオブジェクトにframeworkから使用可能な関数が登録されています。SendMessageSetFullscreenといった関数はModuleに登録されています。JavaScriptからC#スクリプトを呼ぶにはUnityInstanceを介します。UnityInstanceはloader.jsのcreateUnityInstanceの戻り値で取得できます。createUnityInstancePromiseを返すので、ロード完了はthen()で受けます。ロードに成功するとUnityInstanceが取得できるので、以下のようにしてModuleにアクセスできます。

先ほどのprejsで登録したsayHello関数を呼んでみます。WebGLビルドで生成されたindex.htmlを書き換えます。

登録しておいたalert関数が呼べているのが分かります。unityLoaderがframework.jsをロード、続いてロード完了後にunityFramework関数が呼ばれModuleにframework.jsで公開される関数が登録されます。

他にModuleで公開したい関数があれば、prejsやpostsetの仕組みでModuleオブジェクトに登録できそうです。登録に際しては、同名のキーで衝突しないようにprefix等をつけると良いかもしれません。

SendMessage

JavaScriptからC#を呼ぶにはSendMessage関数を使えます。引数で指定したGameObjectの関数を実行することができます。

SendMessage関数は、framework.jsを見ると以下のようになってます。実際には、ccall関数を介してネイティブコードが呼び出されています。

SendMessage関数では引数が1つしか渡せない?ようなので、json形式でシリアライズ/デシリアライズすると複数の引数指定と同等のことができ便利です。

参考リンク

SendMessage以外の方法

MonoPInvokeCallbackという仕組みでできるようです。以下サイトで詳しく説明してくださっているため省略。

Unity(WebGL)でC#の関数からブラウザー側のJavaScript関数を呼び出すまたはその逆(JS⇒C#)に関する知見(プラグイン形式[.jslib]) - Qiita
前書き自分への備忘録もかねて逆引き辞典のような感じで記述します。Emscriptenに関する知識はほとんどないため、Emscripten側から見た説明はほとんどありません。Unity側のC#を…

JavaScriptからシーン内のオブジェクトにテクスチャをセットする

最後に、JavaScript側からシーン内のあるオブジェクトに対して動的にテクスチャをセットしてみます。

C#でテクスチャオブジェクトを生成しマテリアルにセットしています。テクスチャデータの転送は、JavaScript側でtexSubImage2D関数で処理するようにしました。texImage2Dはimmutableと警告が出てしまいます。GetNativeTexturePtr関数でC#側のテクスチャオブジェクトのポインタを得て、jslibで定義しているBindTexture2D関数でポインタを受けとるようにします。

参考リンク

C#から受け取ったポインタにマップされたテクスチャオブジェクトは、GL.textures配列にマップされているようです。テクスチャ画像であるdataには、Imageオブジェクトをセットしています(後述)。

参考リンク

画像データをfetchしてImageオブジェクトを生成します。画像データのロードが完了したら、dataにImageオブジェクトをセットし、SendMessageでC#側のテクスチャセットのメソッドを呼び出しています。その後、C#側からBindTexture2D関数が呼ばれ、テクスチャ画像が転送されます。

以下のような感じで動きます。Cubeにテクスチャを貼ることができました。これで任意の画像をダウンロードしてテクスチャをセットできます。

GL.texturesはいつセットされている?

GL.texturesという配列はいつセットされているのか疑問が生じます。

ブラウザでデバッグするとC#側でnew Texture2Dを実行すると、framework.jsの_glGenTextures関数が呼ばれている?ようでした。実際にコールスタックを追うとwasmのコードから呼ばれています。

 

同じやり方でvideoタグのテクスチャをセットする場合は、以下のような感じでも使えます。以下では、Webカメラの映像をcanvasで画像にしてセットするという方法でやってみました。

参考リンク

コメント

タイトルとURLをコピーしました