凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Unity 5 の WebGL で外部からテクスチャを与える方法について調べてみた

はじめに

Unity 5 WebGL 出力で、外部(自分で書いた JavaScript のコード)からテクスチャを与えたいと思い、色々調べてみましたので情報を共有したいと思います。通常の画像をテクスチャとして指定する方法と、base64 エンコードした画像をテクスチャとして指定する方法の2つを紹介します。

Unity 5 の WebGL については以前の記事をご参照ください。

デモ

後述の「base64 エンコードした画像を与える」の内容になります。Canvas に描いた絵をテクスチャとしてゲーム内で利用する事ができます。

1. 画像をテクスチャとして利用する

自分のサーバに置いてあるテクスチャを反映

以下の様なコードを書いたスクリプトを Cube にアタッチします。

using UnityEngine;
using System.Collections;

public class SetTextureFromURL : MonoBehaviour 
{
    void SetTexture(string url) 
    {
        StartCoroutine( FetchTexture(url) );
    }

    IEnumerator FetchTexture(string url)
    {
        var www = new WWW(url);
        yield return www;
        GetComponent<Renderer>().material.mainTexture = www.texture;
    }
}

これを WebGL ビルドして、SendMessage() 経由でテクスチャをセットしてみます。ここでは index.html と同じ階層に hecomi.png という画像が置いてあるとします。

f:id:hecomi:20141227154505p:plain

問題なく反映されます。

問題

C# での WWW クラスは、JS に変換されると XMLHttpRequest(XHR)を利用する形になります。これにより同じサーバ(同一オリジン)上のリソースや、オリジンの異なるサーバでも明示的にアクセスが許可されているリソースへのアクセスは問題ないのですが、大半の許可されていないオリジンの異なるサーバからテクスチャを WWW を通じて取ってこようとすると問題が生じます。

例えば、Twitter のアイコン画像の URL を先ほどと同じように指定してみます。

f:id:hecomi:20141227155303p:plain

すると、Access-Control-Allow-Origin ヘッダがないから駄目だよ、と怒られます。Web Player の時はブラウザの世界とは別の世界で動いていたため問題にならなかったと思うのですが、WebGL になったことでこういった問題が出てきます。

方法 1: 仲介サーバを立てる

Access-Control-Allow-Origin ヘッダをつけてリソースを仲介するプロキシサーバを立ててあげれば読み込むことが出来ます。例えば Node.js で以下のようなサーバを立てます。

var express = require('express');
var app     = express();
var cors    = require('cors');
var request = require('request');

app.use(cors({
    origin: 'http://127.0.0.1' // 許可するアクセス元
}));

app.get('/:url', function(req, res) {
    var url = decodeURIComponent(req.params.url);
    request(url).pipe(res);
});

app.listen(3000);

リソースへのアクセスをこのサーバ経由にすることでテクスチャを変更することが出来ます。

f:id:hecomi:20141226201325p:plain

なお、一発でやってくれるモジュールもあります。

方法2: corsproxy.com を利用する

方法1 でやったようなサーバを立ててくれているところがあるので、これを利用します。おそらくこれが一番簡単です。

この URL の後ろにアクセスしたい URL を追加してアクセスします。

function convertToCorsProxyURL(url) {
    return 'http://www.corsproxy.com/' + url.replace(/https?:\/\//, '');
}

var imageURL = 'https://pbs.twimg.com/profile_images/548403093939834880/7STViVQ0_400x400.png';
SendMessage('Cube', 'Set', convertToCorsProxyURL(imageURL));

これで Access-Control-Allow-Origin ヘッダ付きな画像として扱うことが出来ます。

方法3: SetPixels する(追記:2014/12/27 23:39)

C# 側で SetPixels() する口を設けてあげる手もあります。

WebGL 出力後の UnityConfig.js を読むと書いてあるのですが、SendMessage() では StringNumber のデータしか送れませんので、データは文字列化して送ることになります。

方法4: 頑張る(ムリ)(追記:2014/12/27 23:39)

WebGL では img タグ等をテクスチャとして利用することが出来ます。具体的には gl.texImage2D() の引数として HTMLImageElement 等を与えることが出来ます。

これが一番速度的に速いと思うのですが、これに対応しようと思うと、おそらく Unity が吐き出した WebGL のコードに手を入れる必要があり、到底人間の作業とは思えないので非現実的だと思います...。Unity WebGL C++ プラグインとかでなんとかなる未来が来ると嬉しいですね。

base64 エンコードした画像を与える

Canvas に描画し、toDataURL()base64 エンコードして文字列にし、それを Unity の世界へ伝えてデコードすることでテクスチャ化することも出来ます。この方法は Web Player でも使える方法で、検索すると幾つか情報が出てきます。

ただし、Access-Control-Allow-Origin ヘッダのついていない画像を描画した際は Tainted Canvas(汚染された Canvas)になってしまい toDataURL() は使えなくなるので注意が必要です(例えば、Twitter のアイコンを Canvas に描画したりするとアウトです)。

まず、Unity 側(C#)で以下のようなコードを書いておきます。

using UnityEngine;

public class SetTextureFromBase64String : MonoBehaviour 
{
    void SetTexture(string base64String)
    {
        var bytes = System.Convert.FromBase64String(base64String);
        var tex = new Texture2D(1, 1);
        tex.LoadImage(bytes);

        GetComponent<Renderer>().material.mainTexture = tex;
    }
}

次に、これを WebGL ビルドした HTML をいじって Canvas を作り、適当にお絵描きをした後に toDataURL() して SendMessage() します。

var canvas = document.getElementById('#paint');
var base64String = canvas.toDataURL().split('base64,')[1]);
SendMessage('Cube', 'SetTexture', base64String);

これで冒頭のデモのようになります。連続して反映することも出来ます。

ただし上記コードのままでは Out of memory になるので注意して下さい。

この方法は結構面白くて、描いた画がゲームの中で動きまわるとか、UV 展開図の塗り絵形式にして自分の塗った色のキャラクタで遊ぶ、みたいなことも出来ますね。

おまけ: スクリーンショットを撮る

上記方法の逆方向を実行すると、Unity の世界から外側の世界に画像を簡単に伝えることが出来ます。

おわりに

例えば Twitter でログインしてもらってその画像をゲーム内に反映させる、みたいなこともこれで可能だと思います。ちなみに、音声に関しても同じように考えることが出来ます。

また、方法 4 の様な高速な方法が分かればもっと色々出来そうです。例えば getUserMedia() 経由でとってきた Webカメラ画を流し込んだりとか可能になると思われます*1。WebRTC で貰ってきた他の PC のカメラ画を映す、みたいなことも簡単にできます。

Web と Unity の融合は色々なコンテンツを産み出せる可能性を秘めていて、とても面白いですね。

*1:Unity 5 WebGL 出力では WebCamTexture はサポートしていません