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

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

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を単体でインストールします。

$ 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にあるサンプルです。

#include <math.h>

extern "C" {
  int int_sqrt(int x) {
    return sqrt(x);
  }
}

ビルドしてみましょう。

$ emcc main.cpp -o function.html -sEXPORTED_FUNCTIONS=_int_sqrt -sEXPORTED_RUNTIME_METHODS=ccall,cwrap

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

$ emrun functions.html

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

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

// 省略

    /**
     * @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は、外部とのインターフェースに用いられるオブジェクトで、ccallcwrap関数が登録されていることが分かります。

mergeInto

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

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

cpp側です。

extern "C"
{
    void my_js(void);
}

int main()
{
    my_js();
    return 1;
}

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

mergeInto(LibraryManager.library, {
  my_js: function () {
    alert("hi");
  },

  my_js2: function () {
    printErr("error");
  },
});

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

$ emcc -o alert.html --js-library library_my.js test.cpp

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

// 省略

  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++から参照されるプロパティのみを含むとありました(おそらくその影響?)

参考リンク: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という方法もあるようです。

Module["pre"] = {
  sayHello() {
    alert("Hello Prejs");
  },
};

ビルドします。

$ emcc -o alert.html --js-library library_my.js --pre-js pre.js test.cpp 

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

// 省略

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オプション指定します)。スクリプトの最後尾に展開されているようです。

// 省略

// 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の展開後に関数を実行するといったことが実現できるようです。

mergeInto(LibraryManager.library, {
  my_js: function () {
    alert("hi");
  },
  my_js__postset: "alert('hello');",
});
$ emcc -o alert.html --js-library library_my2.js test.cpp
// 省略

  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を使うと依存関数を展開できます。

mergeInto(LibraryManager.library, {
  $my_properties: {},
  $method_support: function () {},
  $method_support2: function () {},
  my_js: function () {},
  my_js__deps: ["$my_properties", "$method_support", "$method_support2"],
});
$ emcc -o alert.html --js-library library_my.js test.cpp 

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

// 省略

  var my_properties = {};

  function method_support() {}

  function method_support2() {}
  function _my_js() {}

// 省略

dynCall

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

#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;
}
Module['runtest'] = function() {
  var ptr = _get_func_ptr();
  ret = dynCall('ii', ptr, [1, 2]);
  console.log("sum = " + ret);
};
$ 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をそれぞれ作成しています。

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()
    {
        
    }
}
mergeInto(LibraryManager.library, {
  Hello: function () {
    window.alert("Hello jslib");
  },
  HelloString: function (str) {
    window.alert(Pointer_stringify(str));
  },
});
Module["MySample"] = {
  sayHello() {
    alert("Hello Module");
  },
};

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

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

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

function unityFramework(Module) {
  var Module = typeof Module !== "undefined" ? Module : {};

...

  var shouldRunNow = true;
  if (Module["noInitialRun"]) shouldRunNow = false;
  run();
}

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

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

$ 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のコードを実行できているようです。

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


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

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を書き換えます。

<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の関数を実行することができます。

unityInstance.SendMessage('Cube', 'SetTexture');
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関数を介してネイティブコードが呼び出されています。

  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形式でシリアライズ/デシリアライズすると複数の引数指定と同等のことができ便利です。

unityInstance.SendMessage('Cube', 'SetTexture', JSON.stringify({
  width: cubeImage.width, height: cubeImage.height
}));
    public void SetTexture(string json)
    {
        Debug.Log(json);
    }

参考リンク

SendMessage以外の方法

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

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

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

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

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関数でポインタを受けとるようにします。

参考リンク

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オブジェクトをセットしています(後述)。

参考リンク

<!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はいつセットされている?

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

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

    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;

    }
  function _glGenTextures(n, textures) {
    __glGenObject(n, textures, "createTexture", GL.textures);
  }

 

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

    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();
      })
    }

参考リンク

コメント

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