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を単体でインストールします。
1 2 3 4 5 6 7 8 9 10 11 |
$ git clone https://github.com/emscripten-core/emsdk.git $ cd emsdk $ ./emsdk install latest $ ./emsdk activate latest $ source ./emsdk_env.sh $ emcc -v emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 3.1.24 (68a9f990429e0bcfb63b1cde68bad792554350a5) clang version 16.0.0 (https://github.com/llvm/llvm-project 277c382760bf9575cfa2eac73d5ad1db91466d3f) Target: wasm32-unknown-emscripten Thread model: posix InstalledDir: /Users/guest/workspace/emsdk/upstream/bin |
簡単なサンプルを実行してみましょう。公式docにあるサンプルです。
1 2 3 4 5 6 7 |
#include <math.h> extern "C" { int int_sqrt(int x) { return sqrt(x); } } |
ビルドしてみましょう。
1 |
$ emcc main.cpp -o function.html -sEXPORTED_FUNCTIONS=_int_sqrt -sEXPORTED_RUNTIME_METHODS=ccall,cwrap |
続いてemrun
コマンドでfunction.html
をブラウザでオープンします。
1 |
$ emrun functions.html |
ブラウザコンソールでCの関数を呼んでみます。
ビルドで生成されたfunctions.js
を見てみますと以下のようなコードが展開されています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// 省略 /** * @param {string=} returnType * @param {Array=} argTypes * @param {Object=} opts */ function cwrap(ident, returnType, argTypes, opts) { return function() { return ccall(ident, returnType, argTypes, arguments, opts); } } // 省略 // === Auto-generated postamble setup entry stuff === Module["ccall"] = ccall; Module["cwrap"] = cwrap; var unexportedRuntimeSymbols = [ // 省略 |
cwrap
関数はccall
をコールする無名関数を返しています。Module
は、外部とのインターフェースに用いられるオブジェクトで、ccall
やcwrap
関数が登録されていることが分かります。
mergeInto
Unityでは、.jslib
内でmergeInto
関数を使って、C#からJavaScript関数を呼べるようになります(詳細には、xxx.framework.jsに.jslib
で定義したコードがビルドされ展開されています)。
emscriptenでmergeInto
というインターフェースを提供しているようですので使用例を見てみます。
cpp側です。
1 2 3 4 5 6 7 8 9 10 |
extern "C" { void my_js(void); } int main() { my_js(); return 1; } |
js側です。LibraryManager.library
にマージします。
1 2 3 4 5 6 7 8 9 |
mergeInto(LibraryManager.library, { my_js: function () { alert("hi"); }, my_js2: function () { printErr("error"); }, }); |
以下コマンドでビルドします。--js-library
というオプションでJavaScriptファイルを指定します。
1 |
$ emcc -o alert.html --js-library library_my.js test.cpp |
ビルド後のスクリプトを見てみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// 省略 function warnOnce(text) { if (!warnOnce.shown) warnOnce.shown = {}; if (!warnOnce.shown[text]) { warnOnce.shown[text] = 1; if (ENVIRONMENT_IS_NODE) text = 'warning: ' + text; err(text); } } function _my_js() { // mergeIntoで定義したスクリプト alert("hi"); } var SYSCALLS = {varargs:undefined,get:function() { assert(SYSCALLS.varargs != undefined); SYSCALLS.varargs += 4; var ret = HEAP32[(((SYSCALLS.varargs)-(4))>>2)]; return ret; },getStr:function(ptr) { var ret = UTF8ToString(ptr); return ret; }}; // 省略 |
mergeInto
で定義した関数は、_
(アンダーバー)付きの関数として展開されていることが分かります。
一方、my_js2
関数がビルド後のスクリプトに見当たりません。docsを見ると、emscriptenはスペース節約のためc/c++から参照されるプロパティのみを含むとありました(おそらくその影響?)
prejs、postjs
mergeInto
以外にも--pre-js
や--post-js
という方法もあるようです。
1 2 3 4 5 |
Module["pre"] = { sayHello() { alert("Hello Prejs"); }, }; |
ビルドします。
1 |
$ emcc -o alert.html --js-library library_my.js --pre-js pre.js test.cpp |
結果を見ると以下のようになっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 省略 var Module = typeof Module != 'undefined' ? Module : {}; // See https://caniuse.com/mdn-javascript_builtins_object_assign // See https://caniuse.com/mdn-javascript_builtins_bigint64array // --pre-jses are emitted after the Module integration code, so that they can // refer to Module (if they choose; they can also define Module) Module["pre"] = { // mergeIntoの展開よりも前に入る sayHello() { alert("Hello Prejs"); }, }; // 省略 |
postjsの場合は以下のようになりました(--post-js
オプション指定します)。スクリプトの最後尾に展開されているようです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// 省略 // shouldRunNow refers to calling main(), not run(). var shouldRunNow = true; if (Module['noInitialRun']) shouldRunNow = false; run(); Module["post"] = { sayHello() { alert("Hello Postjs"); }, }; |
__postset
__postset
を使うとmergeInto
の展開後に関数を実行するといったことが実現できるようです。
1 2 3 4 5 6 |
mergeInto(LibraryManager.library, { my_js: function () { alert("hi"); }, my_js__postset: "alert('hello');", }); |
1 |
$ emcc -o alert.html --js-library library_my2.js test.cpp |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
// 省略 function _my_js() { alert("hi"); } var SYSCALLS = {varargs:undefined,get:function() { assert(SYSCALLS.varargs != undefined); SYSCALLS.varargs += 4; var ret = HEAP32[(((SYSCALLS.varargs)-(4))>>2)]; return ret; },getStr:function(ptr) { var ret = UTF8ToString(ptr); return ret; }}; function _proc_exit(code) { EXITSTATUS = code; if (!keepRuntimeAlive()) { if (Module['onExit']) Module['onExit'](code); ABORT = true; } quit_(code, new ExitStatus(code)); } /** @param {boolean|number=} implicit */ function exitJS(status, implicit) { EXITSTATUS = status; checkUnflushedContent(); // if exit() was called explicitly, warn the user if the runtime isn't actually being shut down if (keepRuntimeAlive() && !implicit) { var msg = 'program exited (with status: ' + status + '), but EXIT_RUNTIME is not set, so halting execution but not exiting the runtime or preventing further async execution (build with EXIT_RUNTIME=1, if you want a true shutdown)'; err(msg); } _proc_exit(status); } function handleException(e) { // Certain exception types we do not treat as errors since they are used for // internal control flow. // 1. ExitStatus, which is thrown by exit() // 2. "unwind", which is thrown by emscripten_unwind_to_js_event_loop() and others // that wish to return to JS event loop. if (e instanceof ExitStatus || e == 'unwind') { return EXITSTATUS; } quit_(1, e); } alert('hello'); // ★mergeIntoでマージした関数の後に展開されている var ASSERTIONS = true; // 省略 |
__deps
__deps
を使うと依存関数を展開できます。
1 2 3 4 5 6 7 |
mergeInto(LibraryManager.library, { $my_properties: {}, $method_support: function () {}, $method_support2: function () {}, my_js: function () {}, my_js__deps: ["$my_properties", "$method_support", "$method_support2"], }); |
1 |
$ emcc -o alert.html --js-library library_my.js test.cpp |
展開後は以下のようになりました。$
をつけたプロパティは、ビルド後に展開されたスクリプト内では$
が外れています。
1 2 3 4 5 6 7 8 9 10 |
// 省略 var my_properties = {}; function method_support() {} function method_support2() {} function _my_js() {} // 省略 |
dynCall
dynCallでポインタ経由でネイティブコードを呼び出せます。
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <stdio.h> #include <stdint.h> #include <emscripten.h> int test_add(int a, int b) { return a + b; } EMSCRIPTEN_KEEPALIVE void* get_func_ptr() { return &test_add; } |
1 2 3 4 5 |
Module['runtest'] = function() { var ptr = _get_func_ptr(); ret = dynCall('ii', ptr, [1, 2]); console.log("sum = " + ret); }; |
1 2 |
$ emcc -o index.html --pre-js bind.js -sEXPORTED_FUNCTIONS=_test_add -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE='$dynCall' test.c $ php -S localhost:900 |
Unity jslibとjspre
emscriptenでのJavaScriptコードの指定方法がある程度分かったので、続いてUnity側を見ていきます。
Unityでは、JavaScriptコードをビルド対象に含めるために.jslib
または.jspre
の拡張子を持ったファイルを準備します。emscriptenのビルドで見たように、.jslib
が--js-library
、.jspre
が--pre-js
に相当してそうです。
参考リンク
jslibとjspreを使ったサンプルを作成してみます。
C#のスクリプトは以下のようなサンプルでやってみます。jslibとjspreをそれぞれ作成しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
using UnityEngine; using System.Runtime.InteropServices; public class CubeController : MonoBehaviour { [DllImport("__Internal", EntryPoint = "Hello")] private static extern void HogeHoge(); // C#から呼ぶJavaScriptの関数、別名指定 [DllImport("__Internal")] private static extern void HelloString(string str); // C#から呼ぶJavaScriptの関数 void Start() { HogeHoge(); HelloString("CubeController"); } void Update() { } } |
1 2 3 4 5 6 7 8 |
mergeInto(LibraryManager.library, { Hello: function () { window.alert("Hello jslib"); }, HelloString: function (str) { window.alert(Pointer_stringify(str)); }, }); |
1 2 3 4 5 |
Module["MySample"] = { sayHello() { alert("Hello Module"); }, }; |
DllImport
は、C#スクリプトからJavaScript側のプログラムを呼ベるようにするための指定です。DllImport
のEntryPoint
はビルド後のリンクするシンボル名として渡せそうです。何も指定しないとmergeInto
のプロパティ名をシンボル名として探すようです。上記では、C#からHogeHogeという関数名でJavaScript側のHello関数を呼ぶように指定しています。実際に動かすとちゃんとビルドもパスし関数を呼び出せています。
上記でWebGLビルドした際に生成されるframework.jsを少し覗いてみます。
まず構造は以下のようになっています。
1 2 3 4 5 6 7 8 9 |
function unityFramework(Module) { var Module = typeof Module !== "undefined" ? Module : {}; ... var shouldRunNow = true; if (Module["noInitialRun"]) shouldRunNow = false; run(); } |
emscriptenのビルドで生成されたスクリプトと同じ構造になっており、Unityの場合はunityFramework
という関数でwrapされていることが分かります。
生成されたhtmlをブラウザで開いてみます。Buildという名前でビルド先を指定したとします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
$ cd Build # python3でhttpサーバ起動 $ python3 -m http.server 8000 # nodeでhttpサーバ起動 $ npm install --global http-server $ http-server -p 8000 # phpでhttpサーバ起動 $ php -S localhost:8000 # rubyでhttpサーバ起動 $ ruby -rwebrick -e 'WEBrick::HTTPServer.new(:DocumentRoot => "./", :Port => 8000).start' |
http://localhost:8000 でアクセスしてみます。関数が実行されていることが分かりました。
C#からJavaScriptのコードを実行できているようです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
...省略 // System.Void CubeController::HogeHoge() IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR void CubeController_HogeHoge_mBE683E64D72B180461F18DB8099C5CE7DC26381E (const RuntimeMethod* method) ; // System.Void CubeController::HelloString(System.String) IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR void CubeController_HelloString_mC5A04428ED59E702D9FDB6B94DC6BC8EB838D59B (String_t* ___str0, const RuntimeMethod* method) ; // System.Void UnityEngine.MonoBehaviour::.ctor() IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR void MonoBehaviour__ctor_m592DB0105CA0BC97AA1C5F4AD27B12D68A3B7C1E (MonoBehaviour_t532A11E69716D348D8AA7F854AFCBFCB8AD17F71* __this, const RuntimeMethod* method) ; IL2CPP_EXTERN_C void DEFAULT_CALL Hello(); IL2CPP_EXTERN_C void DEFAULT_CALL HelloString(char*); #ifdef __clang__ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Winvalid-offsetof" #pragma clang diagnostic ignored "-Wunused-variable" #endif #ifdef __clang__ #pragma clang diagnostic pop #endif #ifdef __clang__ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Winvalid-offsetof" #pragma clang diagnostic ignored "-Wunused-variable" #endif // System.Void CubeController::HogeHoge() IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR void CubeController_HogeHoge_mBE683E64D72B180461F18DB8099C5CE7DC26381E (const RuntimeMethod* method) { typedef void (DEFAULT_CALL *PInvokeFunc) (); // Native function invocation reinterpret_cast<PInvokeFunc>(Hello)(); } // System.Void CubeController::HelloString(System.String) IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR void CubeController_HelloString_mC5A04428ED59E702D9FDB6B94DC6BC8EB838D59B (String_t* ___str0, const RuntimeMethod* method) { typedef void (DEFAULT_CALL *PInvokeFunc) (char*); // Marshaling of parameter '___str0' to native representation char* ____str0_marshaled = NULL; ____str0_marshaled = il2cpp_codegen_marshal_string(___str0); // Native function invocation reinterpret_cast<PInvokeFunc>(HelloString)(____str0_marshaled); // Marshaling cleanup of parameter '___str0' native representation il2cpp_codegen_marshal_free(____str0_marshaled); ____str0_marshaled = NULL; } ...省略 |
JavaScriptからC#関数を呼ぶ
続いてJavaScriptからC#の関数を呼ぶ方法について試行していきます。
emcscriptenやUnityのWebGLビルドでは、wasmでC/C++のコードを呼ぶためのUtility関数がビルド後のスクリプト内で使用できるようになっています。この関数インターフェースを使用することで、UnityのC#スクリプトを呼ぶことができそうです。
Module関数
Moduleオブジェクトにframeworkから使用可能な関数が登録されています。SendMessage
やSetFullscreen
といった関数はModuleに登録されています。JavaScriptからC#スクリプトを呼ぶにはUnityInstanceを介します。UnityInstanceはloader.jsのcreateUnityInstance
の戻り値で取得できます。createUnityInstance
はPromise
を返すので、ロード完了はthen()
で受けます。ロードに成功するとUnityInstanceが取得できるので、以下のようにしてModule
にアクセスできます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
let unity; // loader.js内の一部 script.onload = () => { createUnityInstance(canvas, config, (progress) => { progressBarFull.style.width = 100 * progress + "%"; }).then((unityInstance) => { loadingBar.style.display = "none"; fullscreenButton.onclick = () => { unityInstance.SetFullscreen(1); }; unity = unityInstance; // unityInstanceを取得 }).catch((message) => { alert(message); }); }; }; // unity.Module.xxx; // Moduleに登録されているプロパティへのアクセス |
先ほどのprejsで登録したsayHello関数を呼んでみます。WebGLビルドで生成されたindex.html
を書き換えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
<script> ... 省略 let unity; // unityインスタンスを保存する変数 var script = document.createElement("script"); script.src = loaderUrl; script.onload = () => { createUnityInstance(canvas, config, (progress) => { progressBarFull.style.width = 100 * progress + "%"; }).then((unityInstance) => { loadingBar.style.display = "none"; fullscreenButton.onclick = () => { unityInstance.SetFullscreen(1); }; unity = unityInstance; // unityインスタンスを保存 }).catch((message) => { alert(message); }); }; document.body.appendChild(script); // prejsを呼ぶ関数 function invokeCSharp() { unity.Module.MySample.sayHello(); // prejsのsayHelloを呼ぶ } </script> <button type="button" onclick="invokeCSharp()">invokeCSharp</button> </body> ... 省略 |
登録しておいたalert
関数が呼べているのが分かります。unityLoaderがframework.jsをロード、続いてロード完了後にunityFramework
関数が呼ばれModule
にframework.jsで公開される関数が登録されます。
他にModuleで公開したい関数があれば、prejsやpostsetの仕組みでModuleオブジェクトに登録できそうです。登録に際しては、同名のキーで衝突しないようにprefix等をつけると良いかもしれません。
SendMessage
JavaScriptからC#を呼ぶにはSendMessage
関数を使えます。引数で指定したGameObjectの関数を実行することができます。
1 |
unityInstance.SendMessage('Cube', 'SetTexture'); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
using UnityEngine; using System; public class CubeController : MonoBehaviour { [SerializeField] public Renderer obj3d; private Texture2D _texture; public void SetTexture() { Debug.Log("SetTexture"); } void Start() { } void Update() { } } |
SendMessage
関数は、framework.jsを見ると以下のようになってます。実際には、ccall
関数を介してネイティブコードが呼び出されています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
function SendMessage(gameObject, func, param) { if (param === undefined) Module.ccall( "SendMessage", null, ["string", "string"], [gameObject, func], ); else if (typeof param === "string") Module.ccall( "SendMessageString", null, ["string", "string", "string"], [gameObject, func, param], ); else if (typeof param === "number") Module.ccall( "SendMessageFloat", null, ["string", "string", "number"], [gameObject, func, param], ); else throw ( "" + param + " is does not have a type which is supported by SendMessage." ); } |
SendMessage
関数では引数が1つしか渡せない?ようなので、json形式でシリアライズ/デシリアライズすると複数の引数指定と同等のことができ便利です。
1 2 3 |
unityInstance.SendMessage('Cube', 'SetTexture', JSON.stringify({ width: cubeImage.width, height: cubeImage.height })); |
1 2 3 4 |
public void SetTexture(string json) { Debug.Log(json); } |
参考リンク
SendMessage以外の方法
MonoPInvokeCallbackという仕組みでできるようです。以下サイトで詳しく説明してくださっているため省略。
JavaScriptからシーン内のオブジェクトにテクスチャをセットする
最後に、JavaScript側からシーン内のあるオブジェクトに対して動的にテクスチャをセットしてみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
using UnityEngine; using System.Runtime.InteropServices; using System; public class CubeController : MonoBehaviour { [Serializable] public class Image { public int width; public int height; } [SerializeField] public Renderer obj3d; private Texture2D _texture; [DllImport("__Internal")] private static extern void BindTexture2D(int address); public void SetTexture(string json) { var imageProp = JsonUtility.FromJson<Image>(json); Debug.Log(json); if (_texture) { Destroy(_texture); } _texture = new Texture2D(imageProp.width, imageProp.height, TextureFormat.ARGB32, false); BindTexture2D((int)_texture.GetNativeTexturePtr()); obj3d.material.mainTexture = _texture; } void Start() { } void Update() { } } |
C#でテクスチャオブジェクトを生成しマテリアルにセットしています。テクスチャデータの転送は、JavaScript側でtexSubImage2D
関数で処理するようにしました。texImage2D
はimmutableと警告が出てしまいます。GetNativeTexturePtr
関数でC#側のテクスチャオブジェクトのポインタを得て、jslibで定義しているBindTexture2D
関数でポインタを受けとるようにします。
参考リンク
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
mergeInto(LibraryManager.library, { BindTexture2D: function (tex) { // GL.texturesにテクスチャオブジェクトがマップされている。C#からポインタを渡しjs側のtextureオブジェクトを得る GLctx.bindTexture(GLctx.TEXTURE_2D, GL.textures[tex]); GLctx.pixelStorei(GLctx.UNPACK_FLIP_Y_WEBGL, true); GLctx.texSubImage2D( GLctx.TEXTURE_2D, 0, 0, 0, GLctx.RGBA, GLctx.UNSIGNED_BYTE, data, ); GLctx.pixelStorei(GLctx.UNPACK_FLIP_Y_WEBGL, false); }, }); |
C#から受け取ったポインタにマップされたテクスチャオブジェクトは、GL.textures
配列にマップされているようです。テクスチャ画像であるdataには、Image
オブジェクトをセットしています(後述)。
参考リンク
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
<!DOCTYPE html> <html lang="en-us"> <head> <meta charset="utf-8"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Unity WebGL Player | BlogTest</title> <link rel="shortcut icon" href="TemplateData/favicon.ico"> <link rel="stylesheet" href="TemplateData/style.css"> </head> <body> <div id="unity-container" class="unity-desktop"> <canvas id="unity-canvas" width=960 height=600></canvas> <div id="unity-loading-bar"> <div id="unity-logo"></div> <div id="unity-progress-bar-empty"> <div id="unity-progress-bar-full"></div> </div> </div> <div id="unity-warning"> </div> <div id="unity-footer"> <div id="unity-webgl-logo"></div> <div id="unity-fullscreen-button"></div> <div id="unity-build-title">BlogTest</div> </div> </div> <script> var container = document.querySelector("#unity-container"); var canvas = document.querySelector("#unity-canvas"); var loadingBar = document.querySelector("#unity-loading-bar"); var progressBarFull = document.querySelector("#unity-progress-bar-full"); var fullscreenButton = document.querySelector("#unity-fullscreen-button"); var warningBanner = document.querySelector("#unity-warning"); // Shows a temporary message banner/ribbon for a few seconds, or // a permanent error message on top of the canvas if type=='error'. // If type=='warning', a yellow highlight color is used. // Modify or remove this function to customize the visually presented // way that non-critical warnings and error messages are presented to the // user. function unityShowBanner(msg, type) { function updateBannerVisibility() { warningBanner.style.display = warningBanner.children.length ? 'block' : 'none'; } var div = document.createElement('div'); div.innerHTML = msg; warningBanner.appendChild(div); if (type == 'error') div.style = 'background: red; padding: 10px;'; else { if (type == 'warning') div.style = 'background: yellow; padding: 10px;'; setTimeout(function () { warningBanner.removeChild(div); updateBannerVisibility(); }, 5000); } updateBannerVisibility(); } var buildUrl = "Build"; var loaderUrl = buildUrl + "/Build.loader.js"; var config = { dataUrl: buildUrl + "/Build.data", frameworkUrl: buildUrl + "/Build.framework.js", codeUrl: buildUrl + "/Build.wasm", streamingAssetsUrl: "StreamingAssets", companyName: "DefaultCompany", productName: "BlogTest", productVersion: "0.1", showBanner: unityShowBanner, }; // By default Unity keeps WebGL canvas render target size matched with // the DOM size of the canvas element (scaled by window.devicePixelRatio) // Set this to false if you want to decouple this synchronization from // happening inside the engine, and you would instead like to size up // the canvas DOM size and WebGL render target sizes yourself. // config.matchWebGLToCanvasSize = false; if (/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)) { // Mobile device style: fill the whole browser client area with the game canvas: var meta = document.createElement('meta'); meta.name = 'viewport'; meta.content = 'width=device-width, height=device-height, initial-scale=1.0, user-scalable=no, shrink-to-fit=yes'; document.getElementsByTagName('head')[0].appendChild(meta); container.className = "unity-mobile"; // To lower canvas resolution on mobile devices to gain some // performance, uncomment the following line: // config.devicePixelRatio = 1; canvas.style.width = window.innerWidth + 'px'; canvas.style.height = window.innerHeight + 'px'; unityShowBanner('WebGL builds are not supported on mobile devices.'); } else { // Desktop style: Render the game canvas in a window that can be maximized to fullscreen: canvas.style.width = "960px"; canvas.style.height = "600px"; } loadingBar.style.display = "block"; let unity; var script = document.createElement("script"); script.src = loaderUrl; script.onload = () => { createUnityInstance(canvas, config, (progress) => { progressBarFull.style.width = 100 * progress + "%"; }).then((unityInstance) => { loadingBar.style.display = "none"; fullscreenButton.onclick = () => { unityInstance.SetFullscreen(1); }; unity = unityInstance; }).catch((message) => { alert(message); }); }; document.body.appendChild(script); let data; function SetTexture() { fetchImage('/8870101.jpeg', (image) => { data = image; unity.Module.SendMessage('Cube', 'SetTexture', JSON.stringify({ width: image.width, height: image.height })); }); } const fetchImage = (url, callback) => { const image = new Image(); image.onload = () => callback(image) image.src = url; }; </script> <button type="button" onclick="SetTexture()">SetTexture</button><br> </body> </html> |
画像データをfetchしてImage
オブジェクトを生成します。画像データのロードが完了したら、dataにImageオブジェクトをセットし、SendMessage
でC#側のテクスチャセットのメソッドを呼び出しています。その後、C#側からBindTexture2D
関数が呼ばれ、テクスチャ画像が転送されます。
以下のような感じで動きます。Cubeにテクスチャを貼ることができました。これで任意の画像をダウンロードしてテクスチャをセットできます。
GL.texturesという配列はいつセットされているのか疑問が生じます。
ブラウザでデバッグするとC#側でnew Texture2D
を実行すると、framework.jsの_glGenTextures
関数が呼ばれている?ようでした。実際にコールスタックを追うとwasmのコードから呼ばれています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public void SetTexture(string json) { var imageProp = JsonUtility.FromJson<Image>(json); Debug.Log(json); // Texture2D tex = new Texture2D(16, 16, TextureFormat.PVRTC_RGBA4, false); // tex.LoadRawTextureData(ptr); // tex.Apply(); // obj3d.material.mainTexture = tex; if (_texture) { Destroy(_texture); } _texture = new Texture2D(imageProp.width, imageProp.height, TextureFormat.ARGB32, false); BindTexture2D((int)_texture.GetNativeTexturePtr()); // BindTexture2Dば呼ばれる前に_glGenTexturesが呼ばれている obj3d.material.mainTexture = _texture; } |
1 2 3 |
function _glGenTextures(n, textures) { __glGenObject(n, textures, "createTexture", GL.textures); } |
同じやり方でvideoタグのテクスチャをセットする場合は、以下のような感じでも使えます。以下では、Webカメラの映像をcanvasで画像にしてセットするという方法でやってみました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function SetTexture() { const video = document.getElementById('local_video'); const cubeImage = new Image(); cubeImage.onload = function () { data = cubeImage; unity.Module.SendMessage('Cube', 'SetTexture', JSON.stringify({ width: cubeImage.width, height: cubeImage.height })); } video.addEventListener("timeupdate", function () { canvas2.getContext("2d").drawImage(video, 0, 0, 480, 300); cubeImage.src = canvas2.toDataURL(); }) } |
参考リンク
- https://emscripten.org
- https://github.com/emscripten-core/emsdk
- https://blog.unity.com/technology/il2cpp-internals-pinvoke-wrappers
- https://qiita.com/gtk2k/items/9c2679d612e9b5ac7b2e
- https://qiita.com/gtk2k/items/1c7aa7a202d5f96ebdbf
- https://forum.unity.com/threads/texture2d-createexternaltexture-from-a-webgl-texture.1267220/
- https://mbebenita.github.io/WasmExplorer/
- https://wgld.org/d/webgl/w078.html
- https://github.com/emscripten-core/emscripten/tree/7d291a7660cbf678dac9988dda7b898570ba4ed0/test/minimal_webgl
コメント