NFTをUnityで表示する

#NFTをUnity上で表示してみたい

最近（2022年1月現在）、NFTとかメタバース とかめちゃ盛り上がってますね。

メタバースの開発プラットフォームとしてよく採用されるUnityを用いて、WebGL上でethereumネットワークに接続してNFTの画像を取得&表示するプログラムをコーディングしたので忘備録としてまとめます。

なお、Unityの基本的な部分（ボタンの配置とかのレベル）は省略します。例外処理等も実装していません。Javascript未経験から１〜２週間ほどで作ったのでコードが拙いところもありますが多めにみてください...笑

また、AlchmeyやMorlaisというツールを使えば、もっと簡単にできます笑


最終的な完成形は以下。
左のテキストボックスにNFTのコントラクトアドレスとトークンIDを入力すると、右にNFTが表示されます。

[75億円で落札されたアート](https://onlineonly.christies.com/s/beeple-first-5000-days/beeple-b-1981-1/112924)
contractaddress : 0x2a46f2ffd99e19a89476e2f62270e0a35bbf0756
tokenID : 40913
![スクリーンショット 2022-01-18 22.01.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404591/0c598167-235f-383b-411e-3f9273836edf.png)

[なにかわかんないけどかわいい宇宙人](https://opensea.io/assets/0x3b707f0f718f186151b856464e07ddebf22aeb21/573)
contractaddress : 0x3b707f0f718f186151b856464e07DDebF22AEB21!
tokenID : 573

![スクリーンショット 2022-01-21 13.50.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404591/b8175f68-e46c-62f2-6303-51fe01649616.png)





#この記事を読むに際しての前提知識
- ブロックチェーン 周りの知識（[この記事](https://storialaw.jp/blog/8344)を読めば一通り大丈夫です）
    - Ethereum
    - NFT
    - IPFS
- プログラミング、ネットワーク技術周りの知識
    - Javascriptの基本文法
    - Web3.js
    - Unityの基本
    - ネットワーク通信の基本
    

#全体の構成
![スクリーンショット 2022-01-18 22.38.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404591/07794864-551c-9f76-de75-17e495dd72ce.png)




1. WebGL上でコントラクトアドレスとトークンIDを入力
1. Unity内でC#とpluginされたJavascriptにコントラクトアドレスとトークンIDを渡す
1. JavascriptのAPI、Web3.jsを用いてEthereumと接続し、tokenURIを取得
1. tokenURIを用いてIPFSから画像URLを取得
1. 画像URLを用いてWebGL上に画像を描画

以下、この順番で具体的な実装方法を説明します。

#1. WebGL上でコントラクトアドレスとトークンIDを入力

下の図のように、inputFieldを二つ/bottonを一つ/RawImageを一つ　を設置してください。

![スクリーンショット 2022-01-19 16.41.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404591/3451ad32-d020-70d0-619d-c3fe46f4f803.png)


また、Object間及び内部の動きを司るGameController.csを作成し、GameControllerオブジェクトにアタッチしてください。

```c#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Runtime.InteropServices;
using UnityEngine.Networking;
using UnityEngine.UI;
using TMPro;

public class GameController : MonoBehaviour
{
    public TMP_InputField contractAddress;
    public TMP_InputField tokenId;
    public RawImage rawImage;
   
    void Start()
    {
        contractAddress = contractAddress.GetComponent<TMP_InputField>();
        tokenId = tokenId.GetComponent<TMP_InputField>();
        rawImage = rawImage.GetComponent<RawImage>();
    }

    public void InputText()
    {
        fetchURI(contractAddress.text, tokenId.text);
    }
```

さらにbottonが押された時に、InputText()関数が呼び出されるように、BottonのInspectorの"On Click()"を設定してください

#2. Unity内でC#とpluginされたJavascriptにコントラクトアドレスとトークンIDを渡す

続いて、C#とJavascriptのプラグインを行います。
そもそもプラグインする目的は、C#だとEthereumに接続するのが大変そうなので、JavascriptとプラグインしてJavascript-APIのWeb3.jsを用いてEthereumと接続するためです。

プラグインするためにやることは三つあります。

- GameController.cs (C#ファイル）にプラグインのためのC#コードを書く
- web3.jslib（jslibファイル）を作成してJavascriptコードを書く
- index.html（WebGLでBuildする際のdefaultのhtml）を作成する

順番に見ていきます。

### gameController.cs (C#ファイル）にプラグインのためのコードを書く

```C#
public class GameController : MonoBehaviour
{

    [DllImport("__Internal")]
    private static extern void fetchURI(string b, string c);

    void Start()
    {
        ~~~
    }

    public void InputText()
    {
        fetchURI(contractAddress.text, tokenId.text);
    }
```
後に作るJavascript内のfetchURIという関数（引数はコントラクトアドレスとtokenID）をC#から呼び出すために上のようなコードを追加します。詳しくは[公式サイト](https://docs.unity3d.com/ja/2021.2/Manual/webgl-interactingwithbrowserscripting.html)を参照してください。


###web3.jslib（jslibファイル）を作成してコードを書く
UnityのAssets下にPluginsフォルダを作り、そのフォルダ内に web3.jslibファイルを作成する。

```javascript
mergeInto(LibraryManager.library, {
    fetchURI: function (b, c) {
            const contractAddress = Pointer_stringify(b);
            const tokenId = Number(Pointer_stringify(c));
    }
});
```

ポイントはPointer_stringifyを用いて、C#文字列をJavasciript文字列に変更する必要がある点。
詳しくは[公式ドキュメント](https://docs.unity3d.com/ja/2021.2/Manual/webgl-interactingwithbrowserscripting.html)を参照してください
### index.html（WebGLでBuildする際のhtml）をオリジナルで作成する。

UnityのAssets下に、WebGLTemplates>NewTemplate>index.htmlを作成します。
[こちらの記事](https://qiita.com/void_vtuber/items/2aad097eac69771826fd0)がわかりやすく解説してくださってるので参考にしてください。

今回、プラグイン先のJavascript上で用いる外部APIはweb3.jsとnode-fetchなので、それらのCDNをindex.htmlに書き加えます。

```html

<!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 | %UNITY_WEB_NAME%</title>
    <!--使いたいライブラリを追加-->
　　<script src="https://cdn.jsdelivr.net/npm/web3@latest/dist/web3.min.js"></script>
   <script src="https://cdn.jsdelivr.net/npm/node-fetch@3.1.1/src/index.min.js"></script> 

　　<script src="%UNITY_WEBGL_LOADER_URL%"></script>
    <script>
    var gameInstance = UnityLoader.instantiate("gameContainer", "%UNITY_WEBGL_BUILD_URL%");
    </script>
  </head>

  <body>
    <div id="gameContainer" style="width: %UNITY_WIDTH%px; height: %UNITY_HEIGHT%px; margin: auto"></div>
  </body>  
</html>
```



#3. JavascriptのAPI、Web3.jsを用いてEthereumと接続し、tokenURIを取得

Javascript(Jslibファイル）の全体像を最初に示します。下で順番に解説します。（順次書き加えていきます）

```Javascript
mergeInto(LibraryManager.library, {
    fetchURI: function (b, c) {
            //const Web3 = require('web3');
            const rpcURL = "infuraで作成したURL";
            const web3 = new Web3(rpcURL);

            const contractAddress = Pointer_stringify(b);
            const tokenId = Number(Pointer_stringify(c));

            const abi = [
                {
                    "constant": true,
                    "inputs": [
                        {
                            "name": "_tokenId",
                            "type": "uint256"
                        }
                    ],
                    "name": "tokenURI",
                    "outputs": [
                        {
                            "name": "",
                            "type": "string"
                        }
                    ],
                    "payable": false,
                    "stateMutability": "view",
                    "type": "function",
                },
            ];
            new web3.eth.Contract(abi, contractAddress).methods.tokenURI(tokenId).call().then(function (res) {
                       //（次章で書き加える）
            };
        },
    
});
```

[infura](https://infura.io/)（ethereumノードホスティングサービス）に登録して、APIキー（URL）を取得します。
取得したAPIキーを用いて、Web3オブジェクトを生成してください。

全体を通した注意点として、JslibではES6の文法で書くとエラーが発生します。




#4. tokenURIを用いてIPFSから画像URLを取得




```Javascript
mergeInto(LibraryManager.library, {
    fetchURI: function (b, c) {
            //const Web3 = require('web3');
            const rpcURL = "infuraで作成したURL";
            const web3 = new Web3(rpcURL);

            const contractAddress = Pointer_stringify(b);
            const tokenId = Number(Pointer_stringify(c));

            const abi = [
                {
                    "constant": true,
                    "inputs": [
                        {
                            "name": "_tokenId",
                            "type": "uint256"
                        }
                    ],
                    "name": "tokenURI",
                    "outputs": [
                        {
                            "name": "",
                            "type": "string"
                        }
                    ],
                    "payable": false,
                    "stateMutability": "view",
                    "type": "function",
                },
            ];
            new web3.eth.Contract(abi, contractAddress).methods.tokenURI(tokenId).call().then(function (res) {
 
              fetch(res.replace("ipfs://ipfs/","https://ipfs.io/ipfs/"))
              .then(function (data) {
                  return data.json(); // 読み込むデータをJSONに設定
              })
              .then(function (json) {
                  var imgurl = "";
                  if(Object.keys(json).includes("imageUrl")){
                      imgurl = json.imageUrl;
                  }
                  else if(Object.keys(json).includes("image")){
                      imgurl = json.image;
                  }
                  SendMessage("GameController", "ShowImage", imgurl);
              });
          });        
        },
});
```

今回のような、非同期処理の結果をC#に返したい（web3.jsを使ってtokenURIをとってきてそれをC#に返したい）場合は、returnは使えなくてSendMessageする必要性がある。（[ソース](https://gist.github.com/yano3nora/ab222f46abfbec85855b550130aa2b85)）

注意点は、resの中身である

```
ipfs://ipfs/QmPAg1mjxcEQPPtqsLoEcauVedaeMH81WXDPvPx3VC5zUz
```
を使ってfetchしようとすると、二つのエラーにかかる。
ひとつめはnode-fetchがipfsスキームに対応していないことに起因するエラー。もう一つがCORS許可に起因するエラー（下の図）。
![スクリーンショット 2022-01-18 18.56.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404591/c08ec7ec-d2eb-1ea3-d0a8-0aed56b9e5a6.png)


解決策として、まずresのURLのスキームをreplaceを用いてhttps://〜に変更します。

```Javascript
res.replace("ipfs://ipfs/","https://ipfs.io/ipfs/")
```

fetchについては[この記事](https://qiita.com/tarch710/items/91b8abc124a7efbc7a93)がとてもわかりやすいです。

続いてCORS許可に関するエラーの対処です。今回は（時間の制限があるため）、ブラウザの設定を変更するという付け焼き刃的な方法をとりました。（時間ができたときにプロキシサーバを立てた解決策等を実装します。）

クロスサイトスクリプティング等の被害を抑えるために、開発中のみCORS許可することが望ましく、簡単にブラウザのCORS許可のON/OFを切り替えられる[Google Chromeの拡張機能](https://mybrowseraddon.com/access-control-allow-origin.html)がおすすめです。
今回はこれを用いて、CORSを許可します。

また、C#コードgameController.cs内のShowImage関数（次の章で作成）に引数である画像URLを渡す部分に関しては、unityInstance.SendMessageを用います。

#5. 画像URLを用いてWebGL上に画像を描画

最後に、Javascriptから送られてきたImageURLを用いて、C#上で画像を表示するコードを書きます。
[このサイト](https://www.hanachiru-blog.com/entry/2019/07/12/233000)を参考にした。
注意点は、WebGL上で画像を表示するにはブラウザ上でCORSを許可する必要があります。今回は、4で行ったのと同様のCORSを許可します。（本当は望ましくない）


```c#

public class GameController : MonoBehaviour
{
         //（省略）

    public void ShowImage(string uri)
    {
        StartCoroutine(InstallImage(uri));
    }

    private IEnumerator InstallImage(string uri)
    {
        UnityWebRequest www = UnityWebRequestTexture.GetTexture(uri);
        yield return www.SendWebRequest();
        rawImage.texture = ((DownloadHandlerTexture)www.downloadHandler).texture;
    }
}
```

#6. BUILDして実行

最後にBuildのセッティングをしてBuild and Runを行います。

Build SettingからWebGLTemplateを、2で作ったNewTemplateに変更することだけ忘れないでください。

![スクリーンショット 2022-01-20 11.31.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404591/babbd45b-78a3-97e9-8b2d-d505b3b1cdef.png)

このセッティングでBuildをして走らせると、一番最初に紹介したようなアプリケーションがブラウザ上に表示されると思います。

以上です。