Unity WebGLでおえかきした画像の保存

# はじめに
おえかきアプリを作っていて保存機能を実装することになったが、WebGLなので`File.WriteAllBytes()`も`Application.persistentDataPath`使えない…となったので、javascriptプラグインを作ったお話。
※おえかきアプリの実装はメインではないので割愛します。

# 開発環境
- Unity 2020.3.25f1
- Visual Studio Code

# プラグインの作成(.jslib)
Unityには、ブラウザのスクリプトと通信を行う方法が大きく2つあります。  
1. `Application.ExternalCall()` か `SendMessage()`を使う。
2. プラグイン(.jslib)を作り、コードを記述。
今回は後者の方法で実装を進めます。

```jslib:plugin.jslib
mergeInto(LibraryManager.library, {
  ExportTextureJS:function(base64){
    // 手順1
    const image = Pointer_stringify(base64);
  
    // 手順2
    var bin = atob(image.replace(/^.*,/, ''));
    var buffer = new Uint8Array(bin.length);
    for (var i = 0; i < bin.length; i++) {
      buffer[i] = bin.charCodeAt(i);
    }

    try{
      var blob = new Blob([buffer.buffer], {
        type: 'image/png'
      });
    }catch (e){
      return;
    }

    // 手順3
    var url = (window.URL || window.webkitURL);
    var dataUrl = url.createObjectURL(blob);
    
    var a = document.createElement('a');
    a.download = "picture.png";
    a.href = dataUrl;

    a.click();
  },
});
```

## 手順1 javascript文字列への変換
```javascript
const image = Pointer_stringify(base64);
```
まず引数から入ってきたbase64文字列を、ヘルパー関数`Pointer_stringify()`を使用してJavaScript文字列に変換します。
これは文字列がemscripten ヒープ内のポインターとして渡されるためです。


## 手順2 Blob形式への変換
```javascript
var bin = atob(image.replace(/^.*,/, ''));
var buffer = new Uint8Array(bin.length);
for (var i = 0; i < bin.length; i++) {
  buffer[i] = bin.charCodeAt(i);
}

try{
  var blob = new Blob([buffer.buffer], {
    type: 'image/png'
  });
}catch (e){
  return;
}
```
変換した文字列をBlob形式のFileに変換します。

## 手順3 アンカー要素を使用して保存
```javascript
var url = (window.URL || window.webkitURL);
var dataUrl = url.createObjectURL(blob);
    
var a = document.createElement('a');
a.download = "picture.png";
a.href = dataUrl;

a.click();
```
HTMLのアンカー要素`<a>`を作ってdataURLを渡し、クリックイベントを強制的に呼び出します。

# プラグイン呼び出し側の実装(C#)
```cs
using System;
using UnityEngine;
using UnityEngine.UI;
using System.Runtime.InteropServices;

public class ExportTexture : MonoBehaviour
{
#if UNITY_WEBGL && !UNITY_EDITOR
    [DllImport("__Internal")]
    private static extern void ExportTextureJS(string base64);
#endif

	[SerializeField]
	private RawImage _image;

    public void OnPressExport()
	{
#if UNITY_WEBGL && !UNITY_EDITOR
		var texture = (Texture2D)_image.texture;
		var png = texture.EncodeToPNG();
		var base64 = Convert.ToBase64String(png);

        ExportTextureJS("data:image/png;base64," + base64);
#endif
	}
}
```

ブラウザでのみ動くため`#if UNITY_WEBGL && !UNITY_EDITOR` `#endif`で括っています。

## プラグインの関数のインポート
```cs
[DllImport("__Internal")]
private static extern void ExportTextureJS(string base64);
```
jslibに記述した関数を呼び出せるようにしています。
この時、関数名と引数を一致させる必要があります。

## テクスチャのbase64文字列への変換、関数の呼び出し
```cs
var texture = (Texture2D)_image.texture;
var png = texture.EncodeToPNG();
var base64 = Convert.ToBase64String(png);

ExportTextureJS("data:image/png;base64," + base64);
```

今回は`RawImage`を使用しているので、`RawImage`の`texture`を`Texture2D`にキャストして、`EncodeToPNG()`を使用して`byte[]`に変換。さらにそれを`System.Convert.ToBase64String()`に渡してbase64形式に変換しています。

`ExportTextureJS()`に渡す際に`"data:image/png;base64,"`と連結しているのは、今回HTMLのアンカー要素に埋め込む手順を踏んでいるためです。(ただここでやる理由はない)

# 動作物
https://rabitbox.gitlab.io/uintytextureexportwithwebgl/

# おわりに
物凄く雑に紹介しましたが、面倒くさいというのが伝わったなら幸いです。
追伸：PlayCanvasならこんなに面倒くさくない

# リンク・参考記事など
[GitLab](https://gitlab.com/RabitBox/uintytextureexportwithwebgl)
[Unity(WebGL)のC#とブラウザのJavaScriptの連携
](https://note.com/npaka/n/nca1e35a649e3#DXAgG)[JS: base64文字列をBlob形式のFileに変換する
](https://qiita.com/rc_code/items/150003f016287750cf34)