Unityで3DモデルライブラリPolyからランタイムインポート

## Polyはすごい
[Poly](https://poly.google.com/)
[Poly Developpers](https://developers.google.com/poly/)

Polyはさまざまな3Dモデルや360°写真[^1]をCCライセンスで提供しているオンラインライブラリです。
「無料ですぐ使えるモデル置いとくよ！　みんなでAR/VRコンテンツを開発していこうね！」というGoogle様のお慈悲です。
詳しいことは公式とかその周辺記事を漁ってみてください。

[Poly API を使ってみる](https://developers-jp.googleblog.com/2018/01/getting-started-with-poly-api.html)
[[Unity] GooglePolyを使ってローポリ3Dオブジェクトを検索/配置する](https://qiita.com/lycoris102/items/9c8dd2b09abd699e05c4)
[Poly toolkit for unity使った。](http://bibinbaleo.hatenablog.com/entry/2017/12/01/100555)

## Poly Toolkit for Unity
[Poly Toolkit for Unity](https://github.com/googlevr/poly-toolkit-unity)

エディタでロードしている記事はわりとあったんですが、ランタイムロードしている日本語の記事が見つからなかったので使った感触を書きます。2019/04/14時点の情報を元に書いてます。

### Unity QuickStart
[Unity QuickStart](https://developers.google.com/poly/develop/unity)
クイックスタートに従って進めれば問題ありません。スクリーンショットつきの丁寧な説明なのですんなり行けると思います。
Troubleshootingのところに**obsolete APIsを自動アップデートしてね！**とか書いてありますが、これに驚いているようではGoogleのProductを使う**「資格」**はない。[^2]

### API
Polyを使うすべてのシーンに置いておけと書いてある`PolyToolkitManager`の`Prefab`ですが、`PolyApi.Init();`を`Awake`で実行する以外何もしてない……。
いずれ機能が追加されるかもしれないですし、置いとけって書いてあるんだから置いておきましょう。

おそらく公式が想定している手順であろう以下をコードでやっていきます。ついでに`Callback`でつらいので`Task`に変換します。ログイン周りはめんどくさいのでパス。

 1. モデルの基礎情報を取得する
 1. サムネイルを表示する
 1. モデルを`GameObject`としてロード
 1. 権利表記

#### モデルの基礎情報を取得する

##### 検索についての共通
`PolyRequest`を継承した検索条件の`struct`を各検索APIに渡します。
検索結果の`PolyListAssetsResult.nextPageToken`を渡せば次が検索できるので、検索条件はどこかの変数にとっておいて使いまわしましょう。

##### `PolyApi.ListAssets`
普通の検索です。普通。

```c#:ListAssets
public static Task<PolyListAssetsResult> SearchListAssetsAsync(PolyListAssetsRequest assetsRequest)
{
    var tcs = new TaskCompletionSource<PolyListAssetsResult>();
    PolyApi.ListAssets(assetsRequest, result =>
    {
        if (result.Ok)
        {
            tcs.SetResult(result.Value);
        }
        else
        {
            tcs.SetException(new Exception(result.Status.errorMessage));
        }
    });
    return tcs.Task;
}
```

##### `PolyApi.ListUserAssets`
ログインが前提。ユーザが投稿したモデル一覧が検索されます。

```c#
public static Task<PolyListAssetsResult> SearchUserListAssetsAsync(PolyListUserAssetsRequest assetsRequest)
{
    var tcs = new TaskCompletionSource<PolyListAssetsResult>();
    PolyApi.ListUserAssets(assetsRequest, result =>
    {
        if (result.Ok)
        {
            tcs.SetResult(result.Value);
        }
        else
        {
            tcs.SetException(new Exception(result.Status.errorMessage));
        }
    });
    return tcs.Task;
}
```


##### `PolyApi.ListLikedAssets`
ログイン中のユーザがお気に入りしたモデル一覧です。お気に入りはブラウザのサイトからできます。

```c#
public static Task<PolyListAssetsResult> SearchLikedListAssetsAsync(PolyListLikedAssetsRequest assetsRequest)
{
    var tcs = new TaskCompletionSource<PolyListAssetsResult>();
    PolyApi.ListLikedAssets(assetsRequest, result =>
    {
        if (result.Ok)
        {
            tcs.SetResult(result.Value);
        }
        else
        {
            tcs.SetException(new Exception(result.Status.errorMessage));
        }
    });
    return tcs.Task;
}
```

##### `PolyApi.GetAsset`
使うモデルがあらかじめわかっている場合はこちらを使います。ブラウザで見たときの末尾の文字列です。
サンプルやAPIから返ってくる識別子には`assets/`がついていますが、別になくても動きます。それでいいのか。

```c#
public static Task<PolyAsset> GetAssetAsync(string name)
{
    var tcs = new TaskCompletionSource<PolyAsset>();

    PolyApi.GetAsset(name, result =>
    {
        if (result.Ok)
        {
            tcs.SetResult(result.Value);
        }
        else
        {
            tcs.SetException(new Exception(result.Status.errorMessage));
        }
    });

    return tcs.Task;
}
```

#### サムネイルを表示する
##### `PolyApi.FetchThumbnail`
サムネイルを取得します。これをしないと`PolyAsset.thumbnailTexture`はnullのままです。
中では`PolyAsset.thumbnail.url`を読み込んでるだけです。とはいえ`PolyFetchThumbnailOptions`によるサイズ調整とかダウンロード処理のキューイングとかしてくれてるので使っておきましょう。

```c#
public static Task<PolyAsset> FetchThumbnailAsync(PolyAsset asset, PolyFetchThumbnailOptions options)
{
    var tcs = new TaskCompletionSource<PolyAsset>();
    PolyApi.FetchThumbnail(asset, options, (polyAsset, status) =>
    {
        if (status.ok)
        {
            tcs.SetResult(polyAsset);
        }
        else
        {
            tcs.SetException(new Exception(status.errorMessage));
        }
    });

    return tcs.Task;
}
```

#### モデルを`GameObject`としてロード
##### `PolyApi.Import`
ようやくモデルの読み込みです。`PolyImportOptions`の各パラメータの意味はエディタ上でインポートするときの表示と突き合わせると理解しやすいです。Scaleをちゃんと調整しておかないとめっちゃでかいやつが出てきます。

```c#
public static Task<(GameObject obj, PolyAsset polyAsset)> ImportAsync(PolyAsset asset, PolyImportOptions? options = null)
{
    var tcs = new TaskCompletionSource<(GameObject obj, PolyAsset polyAsset)>();

    PolyApi.Import(asset, options ?? PolyImportOptions.Default(), (polyAsset, result) =>
    {
        if (result.Ok)
        {
            tcs.SetResult((result.Value.gameObject, polyAsset));
        }
        else
        {
            tcs.SetException(new Exception(result.Status.errorMessage));
        }
    });
    return tcs.Task;
}

```

#### 権利表記
`PolyApi.GenerateAttributions`はEditor上でインポートしておいたモデルの権利表記に加えてランタイムロードした`PolyAsset`の`List`を渡せば全部まとめて権利表記文を書き出してくれるイカしたAPIです。
引数次第でどちらかだけ出す、なんてこともできます。
ただし第二引数`List<PolyAsset> runtimeAssets`のデフォルトは`null`ですが中でnullチェックしないで触ってます。第二引数なしで蹴ると必ずぬるぽになるので適当にnewしたリストでも入れてあげましょう。ははは。[^3]

ちなみにひとつだけ知りたい、という場合は`PolyAsset.AttributionInfo`で取れます。

### 実践
「人気順にAnimalカテゴリからdogを検索して一番目のモデルをロードして権利表記も表示する」
メソッドをStartで実行してみます。

```c#
private async void SearchAndShowExample()
{
    var searchQuery = PolyListAssetsRequest.Featured();
    // カテゴリ：動物
    searchQuery.category = PolyCategory.ANIMALS;
    // 犬を検索        
    searchQuery.keywords = "dog";
    searchQuery.pageSize = 5;

    var search = await PolyTasker.SearchListAssetsAsync(searchQuery);

    var (obj, asset) = await PolyTasker.ImportAsync(search.assets[0], PolyImportOptions.Default());
    _text.text = asset.AttributionInfo;
}
```

![スクリーンショット 2019-04-16 5.11.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146245/5bd62612-b378-47bc-ca04-b81a1e1d2dfd.png)

出ました！　**狐が。**[^4]
権利表記もちゃんと読み込めています。

### Task化
いままでのtask化したものをまとめてwrapper classにしました。使ってください。
gistは[こちら](https://gist.github.com/nekomimi-daimao/b6695e6974af4b8bf53723ceaaef9be0)。

```c#
using System;
using System.Threading.Tasks;
using PolyToolkit;
using UnityEngine;

namespace Daimao.Nekomimi.Poly
{
    public static class PolyTasker
    {
        public static Task<PolyListAssetsResult> SearchListAssetsAsync(PolyListAssetsRequest assetsRequest)
        {
            var tcs = new TaskCompletionSource<PolyListAssetsResult>();
            PolyApi.ListAssets(assetsRequest, result =>
            {
                if (result.Ok)
                {
                    tcs.SetResult(result.Value);
                }
                else
                {
                    tcs.SetException(new Exception(result.Status.errorMessage));
                }
            });
            return tcs.Task;
        }

        public static Task<PolyListAssetsResult> SearchUserListAssetsAsync(PolyListUserAssetsRequest assetsRequest)
        {
            var tcs = new TaskCompletionSource<PolyListAssetsResult>();
            PolyApi.ListUserAssets(assetsRequest, result =>
            {
                if (result.Ok)
                {
                    tcs.SetResult(result.Value);
                }
                else
                {
                    tcs.SetException(new Exception(result.Status.errorMessage));
                }
            });
            return tcs.Task;
        }


        public static Task<PolyListAssetsResult> SearchLikedListAssetsAsync(PolyListLikedAssetsRequest assetsRequest)
        {
            var tcs = new TaskCompletionSource<PolyListAssetsResult>();
            PolyApi.ListLikedAssets(assetsRequest, result =>
            {
                if (result.Ok)
                {
                    tcs.SetResult(result.Value);
                }
                else
                {
                    tcs.SetException(new Exception(result.Status.errorMessage));
                }
            });
            return tcs.Task;
        }

        public static Task<PolyAsset> FetchThumbnailAsync(PolyAsset asset, PolyFetchThumbnailOptions options)
        {
            var tcs = new TaskCompletionSource<PolyAsset>();
            PolyApi.FetchThumbnail(asset, options, (polyAsset, status) =>
            {
                if (status.ok)
                {
                    tcs.SetResult(polyAsset);
                }
                else
                {
                    tcs.SetException(new Exception(status.errorMessage));
                }
            });

            return tcs.Task;
        }

        public static Task<PolyAsset> GetAssetAsync(string name)
        {
            var tcs = new TaskCompletionSource<PolyAsset>();
            PolyApi.GetAsset(name, result =>
            {
                if (result.Ok)
                {
                    tcs.SetResult(result.Value);
                }
                else
                {
                    tcs.SetException(new Exception(result.Status.errorMessage));
                }
            });

            return tcs.Task;
        }


        public static Task<(GameObject obj, PolyAsset polyAsset)> ImportAsync(PolyAsset asset, PolyImportOptions? options = null)
        {
            var tcs = new TaskCompletionSource<(GameObject obj, PolyAsset polyAsset)>();
            PolyApi.Import(asset, options ?? PolyImportOptions.Default(), (polyAsset, result) =>
            {
                if (result.Ok)
                {
                    tcs.SetResult((result.Value.gameObject, polyAsset));
                }
                else
                {
                    tcs.SetException(new Exception(result.Status.errorMessage));
                }
            });
            return tcs.Task;
        }
    }
}
```

### 気になる点
 - Android, iOSではcacheできない
https://github.com/googlevr/poly-toolkit-unity/issues/9
できなくはないらしい。
ていうかGoogle様！！！！！！これ主にARCoreとかARKitで使ってほしいやつですよね！！！？キャッシュはモバイルにこそ必要！！！！！なんでそこサポートしてないの！！？！？！？！！？！？！

 - `delegate`使ってる 
delegateをどうやったらUniRxで扱えるかわからなかったのと、なんかdelegateは古くてダサいらしいからActionとか使ったほうが……と思ったんですがおそろしく深いところまでcallback渡しに行ってるのでそう簡単に手を入れられない。

 - API制限
PolyもGCPの一部なので呼び出し回数に制限があります。おそらく`Poly Toolkit for Unity`も`Poly API`をラップしただけのもののはずなので[3000 queries-per-minute (QPM)](https://developers.google.com/poly/develop/api)が適応されるはず。でも「もっと！」っておねだりはできるっぽい。

## まとめ
3Dモデル関連はぜんぜん詳しくないのですが、競合サービスと比べてランタイムロードできるのはけっこうな強みのような気がします。でもあんま更新されてなくてGoogle様これ*飽きた*のでは……？
わりと普通に勝てるサービスだと思うのでもっと頑張ってほしいな！

おしまい。


## おまけ
見てて面白かったモデル載せます。見てる感じえろいのは流石にフィルタリングしてるっぽいですが、後は著作権とかない無法の沃野と化してます。[^5]
[公式動画](https://www.youtube.com/watch?v=SbPHn4p9g3c)ですら1:00あたりにやべーやつが大地に立ってるしな！
私的にこういうLowPolyなモデル、特にGoogle様公式のモデルがほんと好きです。大好き。[^6]
iframeの埋め込みが動かないっぽいのでリンクから見てください。

[Doggo DayDream](https://poly.google.com/view/9086wpJW1vO)
かわいい。

[Godzilla](https://poly.google.com/view/cnxFiOuKQcM)
手でコントローラ持ってるのがチャームポイント。

[Roadtrip](https://poly.google.com/view/aLOrYDJMopN)
かわいい。アニメーションしてる。

[Ninja Droid WIP](https://poly.google.com/view/fyiXR6k8JNU)
ニンジャナンデ？　カワイイヤッター！

[Corgi-saurus Rex](https://poly.google.com/view/bP48IoXzB-l)
見えなくはない。

[Turkey Attack!](https://poly.google.com/view/7TOZv5rYzxj)
**Attack!**



[^1]: 360写真の方に家族写真投稿してるやついてめっちゃおもしろい。
[^2]: いつものことです。
[^3]: 殺意すら感じる。
[^4]: ナンデ？
[^5]: 往時のPlayStoreを思い出します。今もわりと無法ですが。
[^6]: [Donut County](http://www.donutcounty.com)とか大好き。